Add a Chart Widget for Settings for in SPA. The chart widget supports line chart, pie chart and bar chart.

Test: unit test and manually test on device
Bug: 244401879
Change-Id: I7adf48c71452dd0a995d548b7ef6e03e02a8b665
This commit is contained in:
Kelly
2022-09-08 17:27:19 +08:00
parent 56d59a1c0b
commit 3a4e82ccc2
12 changed files with 885 additions and 0 deletions

View File

@@ -58,4 +58,5 @@ android {
dependencies {
implementation(project(":spa"))
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0-alpha"
}

View File

@@ -24,6 +24,7 @@ import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
import com.android.settingslib.spa.gallery.home.HomePageProvider
import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
import com.android.settingslib.spa.gallery.page.ChartPageProvider
import com.android.settingslib.spa.gallery.page.FooterPageProvider
import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
@@ -69,6 +70,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) {
CategoryPageProvider,
ActionButtonPageProvider,
ProgressBarPageProvider,
ChartPageProvider,
),
rootPages = listOf(
HomePageProvider.createSettingsPage(),

View File

@@ -29,6 +29,7 @@ import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
import com.android.settingslib.spa.gallery.page.ArgumentPageModel
import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
import com.android.settingslib.spa.gallery.page.ChartPageProvider
import com.android.settingslib.spa.gallery.page.FooterPageProvider
import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
@@ -56,6 +57,7 @@ object HomePageProvider : SettingsPageProvider {
CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
)
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.spa.gallery.page
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.chart.BarChart
import com.android.settingslib.spa.widget.chart.BarChartData
import com.android.settingslib.spa.widget.chart.BarChartModel
import com.android.settingslib.spa.widget.chart.LineChart
import com.android.settingslib.spa.widget.chart.LineChartData
import com.android.settingslib.spa.widget.chart.LineChartModel
import com.android.settingslib.spa.widget.chart.PieChart
import com.android.settingslib.spa.widget.chart.PieChartData
import com.android.settingslib.spa.widget.chart.PieChartModel
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.github.mikephil.charting.formatter.IAxisValueFormatter
import java.text.NumberFormat
private enum class WeekDay(val num: Int) {
Sun(0), Mon(1), Tue(2), Wed(3), Thu(4), Fri(5), Sat(6),
}
private const val TITLE = "Sample Chart"
object ChartPageProvider : SettingsPageProvider {
override val name = "Chart"
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
val owner = SettingsPage.create(name)
val entryList = mutableListOf<SettingsEntry>()
entryList.add(
SettingsEntryBuilder.create("Line Chart", owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = "Line Chart"
})
LineChart(
lineChartModel = object : LineChartModel {
override val chartDataList = listOf(
LineChartData(x = 0f, y = 0f),
LineChartData(x = 1f, y = 0.1f),
LineChartData(x = 2f, y = 0.2f),
LineChartData(x = 3f, y = 0.6f),
LineChartData(x = 4f, y = 0.9f),
LineChartData(x = 5f, y = 1.0f),
LineChartData(x = 6f, y = 0.8f),
)
override val xValueFormatter =
IAxisValueFormatter { value, _ ->
"${WeekDay.values()[value.toInt()]}"
}
override val yValueFormatter =
IAxisValueFormatter { value, _ ->
NumberFormat.getPercentInstance().format(value)
}
}
)
}.build()
)
entryList.add(
SettingsEntryBuilder.create("Bar Chart", owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = "Bar Chart"
})
BarChart(
barChartModel = object : BarChartModel {
override val chartDataList = listOf(
BarChartData(x = 0f, y = 12f),
BarChartData(x = 1f, y = 5f),
BarChartData(x = 2f, y = 21f),
BarChartData(x = 3f, y = 5f),
BarChartData(x = 4f, y = 10f),
BarChartData(x = 5f, y = 9f),
BarChartData(x = 6f, y = 1f),
)
override val xValueFormatter =
IAxisValueFormatter { value, _ ->
"${WeekDay.values()[value.toInt()]}"
}
override val yValueFormatter =
IAxisValueFormatter { value, _ ->
"${value.toInt()}m"
}
override val yAxisMaxValue = 30f
}
)
}.build()
)
entryList.add(
SettingsEntryBuilder.create("Pie Chart", owner)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = "Pie Chart"
})
PieChart(
pieChartModel = object : PieChartModel {
override val chartDataList = listOf(
PieChartData(label = "Settings", value = 20f),
PieChartData(label = "Chrome", value = 5f),
PieChartData(label = "Gmail", value = 3f),
)
override val centerText = "Today"
}
)
}.build()
)
return entryList
}
fun buildInjectEntry(): SettingsEntryBuilder {
return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
.setIsAllowSearch(true)
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = TITLE
override val onClick = navigator(name)
})
}
}
@Composable
override fun Page(arguments: Bundle?) {
RegularScaffold(title = TITLE) {
for (entry in buildEntry(arguments)) {
entry.UiLayout(arguments)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun ChartPagePreview() {
SettingsTheme {
ChartPageProvider.Page(null)
}
}

View File

@@ -26,6 +26,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io"}
}
}
rootProject.name = "SpaLib"

View File

@@ -33,6 +33,7 @@ android_library {
"androidx.navigation_navigation-compose",
"com.google.android.material_material",
"lottie_compose",
"MPAndroidChart",
],
kotlincflags: [
"-Xjvm-default=all",

View File

@@ -64,4 +64,5 @@ dependencies {
api "com.google.android.material:material:1.7.0-alpha03"
debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version"
implementation "com.airbnb.android:lottie-compose:5.2.0"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0-alpha"
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.spa.widget.chart
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.settingslib.spa.framework.theme.divider
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.formatter.IAxisValueFormatter
/**
* The chart settings model for [BarChart].
*/
interface BarChartModel {
/**
* The chart data list for [BarChart].
*/
val chartDataList: List<BarChartData>
/**
* The label text formatter for x value.
*/
val xValueFormatter: IAxisValueFormatter?
get() = null
/**
* The label text formatter for y value.
*/
val yValueFormatter: IAxisValueFormatter?
get() = null
/**
* The minimum value for y-axis.
*/
val yAxisMinValue: Float
get() = 0f
/**
* The maximum value for y-axis.
*/
val yAxisMaxValue: Float
get() = 1f
/**
* The label count for y-axis.
*/
val yAxisLabelCount: Int
get() = 3
}
data class BarChartData(
var x: Float?,
var y: Float?,
)
@Composable
fun BarChart(barChartModel: BarChartModel) {
BarChart(
chartDataList = barChartModel.chartDataList,
xValueFormatter = barChartModel.xValueFormatter,
yValueFormatter = barChartModel.yValueFormatter,
yAxisMinValue = barChartModel.yAxisMinValue,
yAxisMaxValue = barChartModel.yAxisMaxValue,
yAxisLabelCount = barChartModel.yAxisLabelCount,
)
}
@Composable
fun BarChart(
chartDataList: List<BarChartData>,
modifier: Modifier = Modifier,
xValueFormatter: IAxisValueFormatter? = null,
yValueFormatter: IAxisValueFormatter? = null,
yAxisMinValue: Float = 0f,
yAxisMaxValue: Float = 30f,
yAxisLabelCount: Int = 4,
) {
Column(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column(
modifier = Modifier
.padding(16.dp)
.height(170.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val colorScheme = MaterialTheme.colorScheme
val labelTextColor = colorScheme.onSurfaceVariant.toArgb()
val labelTextSize = MaterialTheme.typography.bodyMedium.fontSize.value
Crossfade(targetState = chartDataList) { barChartData ->
AndroidView(factory = { context ->
BarChart(context).apply {
// Fixed Settings.
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
this.description.isEnabled = false
this.legend.isEnabled = false
this.extraBottomOffset = 4f
this.setScaleEnabled(false)
this.xAxis.position = XAxis.XAxisPosition.BOTTOM
this.xAxis.setDrawGridLines(false)
this.xAxis.setDrawAxisLine(false)
this.xAxis.textColor = labelTextColor
this.xAxis.textSize = labelTextSize
this.xAxis.yOffset = 10f
this.axisLeft.isEnabled = false
this.axisRight.setDrawAxisLine(false)
this.axisRight.textSize = labelTextSize
this.axisRight.textColor = labelTextColor
this.axisRight.gridColor = colorScheme.divider.toArgb()
this.axisRight.xOffset = 10f
// Customizable Settings.
this.xAxis.valueFormatter = xValueFormatter
this.axisRight.valueFormatter = yValueFormatter
this.axisLeft.axisMinimum = yAxisMinValue
this.axisLeft.axisMaximum = yAxisMaxValue
this.axisRight.axisMinimum = yAxisMinValue
this.axisRight.axisMaximum = yAxisMaxValue
this.axisRight.setLabelCount(yAxisLabelCount, true)
}
},
modifier = Modifier
.wrapContentSize()
.padding(4.dp),
update = {
updateBarChartWithData(it, barChartData, colorScheme)
}
)
}
}
}
}
fun updateBarChartWithData(
chart: BarChart,
data: List<BarChartData>,
colorScheme: ColorScheme
) {
val entries = ArrayList<BarEntry>()
for (i in data.indices) {
val item = data[i]
entries.add(BarEntry(item.x ?: 0.toFloat(), item.y ?: 0.toFloat()))
}
val ds = BarDataSet(entries, "")
ds.colors = arrayListOf(colorScheme.surfaceVariant.toArgb())
ds.setDrawValues(false)
ds.isHighlightEnabled = true
ds.highLightColor = colorScheme.primary.toArgb()
ds.highLightAlpha = 255
// TODO: Sets round corners for bars.
val d = BarData(ds)
chart.data = d
chart.invalidate()
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.spa.widget.chart
import androidx.compose.ui.graphics.Color
object ColorPalette {
// Alpha = 1
val red: Color = Color(0xffd93025)
val orange: Color = Color(0xffe8710a)
val yellow: Color = Color(0xfff9ab00)
val green: Color = Color(0xff1e8e3e)
val cyan: Color = Color(0xff12b5cb)
val blue: Color = Color(0xff1a73e8)
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.spa.widget.chart
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.settingslib.spa.framework.theme.divider
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.formatter.IAxisValueFormatter
/**
* The chart settings model for [LineChart].
*/
interface LineChartModel {
/**
* The chart data list for [LineChart].
*/
val chartDataList: List<LineChartData>
/**
* The label text formatter for x value.
*/
val xValueFormatter: IAxisValueFormatter?
get() = null
/**
* The label text formatter for y value.
*/
val yValueFormatter: IAxisValueFormatter?
get() = null
/**
* The minimum value for y-axis.
*/
val yAxisMinValue: Float
get() = 0f
/**
* The maximum value for y-axis.
*/
val yAxisMaxValue: Float
get() = 1f
/**
* The label count for y-axis.
*/
val yAxisLabelCount: Int
get() = 3
/**
* Indicates whether to smooth the line.
*/
val showSmoothLine: Boolean
get() = true
}
data class LineChartData(
var x: Float?,
var y: Float?,
)
@Composable
fun LineChart(lineChartModel: LineChartModel) {
LineChart(
chartDataList = lineChartModel.chartDataList,
xValueFormatter = lineChartModel.xValueFormatter,
yValueFormatter = lineChartModel.yValueFormatter,
yAxisMinValue = lineChartModel.yAxisMinValue,
yAxisMaxValue = lineChartModel.yAxisMaxValue,
yAxisLabelCount = lineChartModel.yAxisLabelCount,
showSmoothLine = lineChartModel.showSmoothLine,
)
}
@Composable
fun LineChart(
chartDataList: List<LineChartData>,
modifier: Modifier = Modifier,
xValueFormatter: IAxisValueFormatter? = null,
yValueFormatter: IAxisValueFormatter? = null,
yAxisMinValue: Float = 0f,
yAxisMaxValue: Float = 1f,
yAxisLabelCount: Int = 3,
showSmoothLine: Boolean = true,
) {
Column(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column(
modifier = Modifier
.padding(16.dp)
.height(170.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val colorScheme = MaterialTheme.colorScheme
val labelTextColor = colorScheme.onSurfaceVariant.toArgb()
val labelTextSize = MaterialTheme.typography.bodyMedium.fontSize.value
Crossfade(targetState = chartDataList) { lineChartData ->
AndroidView(factory = { context ->
LineChart(context).apply {
// Fixed Settings.
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
this.description.isEnabled = false
this.legend.isEnabled = false
this.extraBottomOffset = 4f
this.setTouchEnabled(false)
this.xAxis.position = XAxis.XAxisPosition.BOTTOM
this.xAxis.setDrawGridLines(false)
this.xAxis.setDrawAxisLine(false)
this.xAxis.textColor = labelTextColor
this.xAxis.textSize = labelTextSize
this.xAxis.yOffset = 10f
this.axisLeft.isEnabled = false
this.axisRight.setDrawAxisLine(false)
this.axisRight.textSize = labelTextSize
this.axisRight.textColor = labelTextColor
this.axisRight.gridColor = colorScheme.divider.toArgb()
this.axisRight.xOffset = 10f
this.axisRight.isGranularityEnabled = true
// Customizable Settings.
this.xAxis.valueFormatter = xValueFormatter
this.axisRight.valueFormatter = yValueFormatter
this.axisLeft.axisMinimum =
yAxisMinValue - 0.01f * (yAxisMaxValue - yAxisMinValue)
this.axisRight.axisMinimum =
yAxisMinValue - 0.01f * (yAxisMaxValue - yAxisMinValue)
this.axisRight.granularity =
(yAxisMaxValue - yAxisMinValue) / (yAxisLabelCount - 1)
}
},
modifier = Modifier
.wrapContentSize()
.padding(4.dp),
update = {
updateLineChartWithData(it, lineChartData, colorScheme, showSmoothLine)
})
}
}
}
}
fun updateLineChartWithData(
chart: LineChart,
data: List<LineChartData>,
colorScheme: ColorScheme,
showSmoothLine: Boolean
) {
val entries = ArrayList<Entry>()
for (i in data.indices) {
val item = data[i]
entries.add(Entry(item.x ?: 0.toFloat(), item.y ?: 0.toFloat()))
}
val ds = LineDataSet(entries, "")
ds.colors = arrayListOf(colorScheme.primary.toArgb())
ds.lineWidth = 2f
if (showSmoothLine) {
ds.mode = LineDataSet.Mode.CUBIC_BEZIER
}
ds.setDrawValues(false)
ds.setDrawCircles(false)
ds.setDrawFilled(true)
ds.fillColor = colorScheme.primary.toArgb()
ds.fillAlpha = 38
// TODO: enable gradient fill color for line chart.
val d = LineData(ds)
chart.data = d
chart.invalidate()
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.spa.widget.chart
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.github.mikephil.charting.charts.PieChart
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
/**
* The chart settings model for [PieChart].
*/
interface PieChartModel {
/**
* The chart data list for [PieChart].
*/
val chartDataList: List<PieChartData>
/**
* The center text in the hole of [PieChart].
*/
val centerText: String?
get() = null
}
val colorPalette = arrayListOf(
ColorPalette.blue.toArgb(),
ColorPalette.red.toArgb(),
ColorPalette.yellow.toArgb(),
ColorPalette.green.toArgb(),
ColorPalette.orange.toArgb(),
ColorPalette.cyan.toArgb(),
Color.Blue.toArgb()
)
data class PieChartData(
var value: Float?,
var label: String?,
)
@Composable
fun PieChart(pieChartModel: PieChartModel) {
PieChart(
chartDataList = pieChartModel.chartDataList,
centerText = pieChartModel.centerText,
)
}
@Composable
fun PieChart(
chartDataList: List<PieChartData>,
modifier: Modifier = Modifier,
centerText: String? = null,
) {
Column(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column(
modifier = Modifier
.padding(16.dp)
.height(280.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val colorScheme = MaterialTheme.colorScheme
val labelTextColor = colorScheme.onSurfaceVariant.toArgb()
val labelTextSize = MaterialTheme.typography.bodyMedium.fontSize.value
val centerTextSize = MaterialTheme.typography.titleLarge.fontSize.value
Crossfade(targetState = chartDataList) { pieChartData ->
AndroidView(factory = { context ->
PieChart(context).apply {
// Fixed settings.`
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
this.isRotationEnabled = false
this.description.isEnabled = false
this.legend.isEnabled = false
this.setTouchEnabled(false)
this.isDrawHoleEnabled = true
this.holeRadius = 90.0f
this.setHoleColor(Color.Transparent.toArgb())
this.setEntryLabelColor(labelTextColor)
this.setEntryLabelTextSize(labelTextSize)
this.setCenterTextSize(centerTextSize)
this.setCenterTextColor(colorScheme.onSurface.toArgb())
// Customizable settings.
this.centerText = centerText
}
},
modifier = Modifier
.wrapContentSize()
.padding(4.dp), update = {
updatePieChartWithData(it, pieChartData)
})
}
}
}
}
fun updatePieChartWithData(
chart: PieChart,
data: List<PieChartData>,
) {
val entries = ArrayList<PieEntry>()
for (i in data.indices) {
val item = data[i]
entries.add(PieEntry(item.value ?: 0.toFloat(), item.label ?: ""))
}
val ds = PieDataSet(entries, "")
ds.setDrawValues(false)
ds.colors = colorPalette
ds.sliceSpace = 2f
ds.yValuePosition = PieDataSet.ValuePosition.OUTSIDE_SLICE
ds.xValuePosition = PieDataSet.ValuePosition.OUTSIDE_SLICE
ds.valueLineColor = Color.Transparent.toArgb()
ds.valueLinePart1Length = 0.1f
ds.valueLinePart2Length = 0f
val d = PieData(ds)
chart.data = d
chart.invalidate()
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.spa.widget
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.widget.chart.BarChart
import com.android.settingslib.spa.widget.chart.BarChartData
import com.android.settingslib.spa.widget.chart.LineChart
import com.android.settingslib.spa.widget.chart.LineChartData
import com.android.settingslib.spa.widget.chart.PieChart
import com.android.settingslib.spa.widget.chart.PieChartData
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ChartTest {
@get:Rule
val composeTestRule = createComposeRule()
private val Chart = SemanticsPropertyKey<String>("Chart")
private var SemanticsPropertyReceiver.chart by Chart
private fun hasChart(chart: String): SemanticsMatcher =
SemanticsMatcher.expectValue(Chart, chart)
@Test
fun line_chart_displayed() {
composeTestRule.setContent {
LineChart(
chartDataList = listOf(
LineChartData(x = 0f, y = 0f),
LineChartData(x = 1f, y = 0.1f),
LineChartData(x = 2f, y = 0.2f),
LineChartData(x = 3f, y = 0.7f),
LineChartData(x = 4f, y = 0.9f),
LineChartData(x = 5f, y = 1.0f),
LineChartData(x = 6f, y = 0.8f),
),
modifier = Modifier.semantics { chart = "line" }
)
}
composeTestRule.onNode(hasChart("line")).assertIsDisplayed()
}
@Test
fun bar_chart_displayed() {
composeTestRule.setContent {
BarChart(
chartDataList = listOf(
BarChartData(x = 0f, y = 12f),
BarChartData(x = 1f, y = 5f),
BarChartData(x = 2f, y = 21f),
BarChartData(x = 3f, y = 5f),
BarChartData(x = 4f, y = 10f),
BarChartData(x = 5f, y = 9f),
BarChartData(x = 6f, y = 1f),
),
yAxisMaxValue = 30f,
modifier = Modifier.semantics { chart = "bar" }
)
}
composeTestRule.onNode(hasChart("bar")).assertIsDisplayed()
}
@Test
fun pie_chart_displayed() {
composeTestRule.setContent {
PieChart(
chartDataList = listOf(
PieChartData(label = "Settings", value = 20f),
PieChartData(label = "Chrome", value = 5f),
PieChartData(label = "Gmail", value = 3f),
),
centerText = "Today",
modifier = Modifier.semantics { chart = "pie" }
)
}
composeTestRule.onNode(hasChart("pie")).assertIsDisplayed()
}
}