Merge "Add a Chart Widget for Settings for in SPA. The chart widget supports line chart, pie chart and bar chart."
This commit is contained in:
@@ -58,4 +58,5 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":spa"))
|
||||
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0-alpha"
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io"}
|
||||
}
|
||||
}
|
||||
rootProject.name = "SpaLib"
|
||||
|
||||
@@ -33,6 +33,7 @@ android_library {
|
||||
"androidx.navigation_navigation-compose",
|
||||
"com.google.android.material_material",
|
||||
"lottie_compose",
|
||||
"MPAndroidChart",
|
||||
],
|
||||
kotlincflags: [
|
||||
"-Xjvm-default=all",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user