跳到正文 Graphs · Jimmy Huang's Blog
← Week 10 Location-Aware Computing 学习 › C6001 › 2026 S1 › FIT5046 Mobile and distributed computing systems › Week 10 Location-Aware Computing Graphs Graphs 本页目录· 43 条 Lab学完总结 › Overview 本周 Additional Lab 的主题是 Graphs / Charts in Android ,重点是学习如何在 Android App 中显示图表,包括 Pie Chart(饼图) 和 Bar Chart(柱状图) 。本周会接触两种实现方式:一种是使用第三方图表库 MPAndroidChart ,另一种是直接用 Jetpack Compose 自己画图。:contentReferenceoaicite:0
这周的主线可以理解为:
先学会如何在 Android 项目中加入外部仓库和第三方依赖;
再用 MPAndroidChart 快速做出 Bar Chart 和 Pie Chart;
然后再用 Jetpack Compose 原生方式实现简单图表;
Goal 完成本周 Lab 后,你应该能够:
创建一个 Android Compose 项目;
在 Gradle 中添加外部 Maven repository;
添加并使用 MPAndroidChart 第三方图表库;
使用 AndroidView 在 Compose 中嵌入传统 Android View;
本页目录 使用 FilterChip 在 Pie Chart 和 Bar Chart 之间切换;
使用 Compose layout 自己实现简单 Bar Chart;
使用 Compose Canvas 和 drawArc() 自己实现简单 Pie Chart;
解释第三方图表库和 Compose 自绘图表的区别。
Task 0: 创建 GraphLab 项目 本 Task 要先准备一个干净的 Android 项目,后面所有图表代码都会放在这个项目里。
Step 0.1:创建新 Android 项目 点击 New Project
选择 Empty Activity
Project name 填:
UI 选择 Compose 模板
点击 Finish
你是在创建一个最小可运行的 Compose App。后面的 Pie Chart 和 Bar Chart 都会写在这个项目里面。
本周 Lab 的目标不是做复杂 App,而是学习图表本身,所以用一个干净项目最方便排错。
Step 0.2:先运行一次空项目 点击 Android Studio 顶部的绿色运行按钮,先确认默认项目能正常启动。
Emulator 可以打开;
默认 Compose 页面可以显示;
项目没有 Gradle sync error;
没有 Kotlin 编译错误。
Task 1: 添加 MPAndroidChart 外部依赖 本 Task 要把第三方图表库 MPAndroidChart 加入项目。
在 settings.gradle.kts 中添加 JitPack repository;
在 module-level build.gradle.kts 中添加 MPAndroidChart dependency;
如果 Android Studio 建议使用 Version Catalog,就接受建议;
Sync Gradle;
确认依赖可以正常下载。
Step 1.1:打开 settings.gradle.kts 如果你的项目使用的是 Groovy DSL,文件名可能是:
Step 1.2:添加 JitPack repository 找到 dependencyResolutionManagement 这一段,在 repositories 里面加入:
MPAndroidChart 这个库需要通过 JitPack 下载,所以我们要告诉 Gradle:除了 Google 和 Maven Central,也去 JitPack 找依赖。
如果不添加 JitPack,后面写了 dependency,Gradle 也可能找不到这个库。
这是 Groovy DSL 写法,不适用于 settings.gradle.kts。
Step 1.3:添加 MPAndroidChart dependency 如果 Android Studio 提示你把 dependency 转成 Version Catalog,可以接受建议。
Step 1.4:如果使用 Version Catalog,检查 libs.versions.toml 如果你接受了 Android Studio 的建议,它可能会自动修改:
然后在 app/build.gradle.kts 中,dependency 可能会变成:
libs.versions.toml 是新版 Android 项目常见的依赖集中管理方式。它把依赖版本集中放在一个地方,方便以后统一修改。
如果你的项目没有自动转成 Version Catalog,也可以继续保留:
Step 1.5:验证 Gradle Sync 成功
Task 2: 使用 MPAndroidChart 创建 Pie Chart 和 Bar Chart 本 Task 会使用第三方库 MPAndroidChart 创建两个图表:
Step 2.1:替换 MainActivity.kt
Step 2.2:理解 LibraryGraphScreen 在做什么 selectedMode 保存当前用户选择的是 Pie Chart 还是 Bar Chart;
remember 让这个状态在 Compose 重组时保留下来;
mutableStateOf 表示这个值变化时,UI 会自动刷新;
modes 是可选图表类型。
图表切换本质上是一个 UI state 问题。用户点不同的 FilterChip, selectedMode 改变,页面就显示不同图表。
Step 2.3:理解 FilterChip 在做什么 为什么用 FilterChip 而不是 Button:
这里 Pie Chart 和 Bar Chart 是同一组数据的两种查看方式,所以它们更像一个“切换选项”,不是两个完全独立操作。 FilterChip 比普通 Button 更适合表达这种“当前选中状态”。
Step 2.4:理解 LibraryBarChartScreen 在做什么 每一个 BarEntry(x, y) 代表一个柱子:
x 是横轴位置;
y 是柱子的高度;
这里用一周每天的步数作为例子。
为什么要用 IndexAxisValueFormatter:
因为 BarEntry 的 x 值是数字,例如 0f, 1f, 2f。但用户看图表时不想看数字,而是想看星期几,所以我们要把数字位置转换成文字标签。
Step 2.5:理解 AndroidView 在做什么 MPAndroidChart 是传统 Android View 库,不是原生 Compose 组件。
AndroidView 是 Compose 和传统 Android View 之间的桥梁。
Compose 页面默认只能直接放 Compose 组件;
MPAndroidChart 的 BarChart 和 PieChart 是传统 View;
所以必须用 AndroidView 把它嵌入 Compose。
AndroidView 就是把旧 View 系统里的 UI 元件放进 Compose 页面里。
Step 2.6:理解 LibraryPieChartScreen 在做什么 每一个 PieEntry(value, label) 代表饼图的一块:
35f, "Bills" 表示 Bills 占 35;
50f, "Mortgage" 表示 Mortgage 占 50;
这些数字加起来刚好是 100,所以显示成百分比很直观。
Step 2.7:理解 PercentValueFormatter 在做什么 Pie Chart 通常表达比例关系,所以显示百分比比显示普通数字更容易理解。
Task 3: 运行并验证 MPAndroidChart 图表
Step 3.1:运行 App 点击 Android Studio 顶部绿色运行按钮。
Step 3.2:切换到 Bar Chart 页面切换成 Bar Chart;
x-axis 显示一周日期;
y-axis 显示步数;
图表以动画形式出现。
Task 4: 使用 Jetpack Compose 自己创建 Bar Chart 和 Pie Chart 本 Task 会实现第二种方式:不依赖 MPAndroidChart,而是用 Compose 自己画图。
用 Compose layout 实现 Bar Chart;
用 Compose Canvas 实现 Pie Chart;
继续用 FilterChip 切换图表;
理解 Bar Chart 和 Pie Chart 为什么实现方式不同。
为了避免和 MPAndroidChart 的 BarChart / PieChart 类重名,本教程把 Compose 自绘版本命名为:
Step 4.1:替换 MainActivity.kt 为 Compose 自绘版本 如果你已经完成 Task 2,可以先复制保存 Task 2 的代码截图或提交记录。
然后把 MainActivity.kt 替换成下面这一版。
Step 4.2:理解 ComposeBarChart 在做什么 所以 Food 的柱子宽度就是可用宽度的 95%。
Compose 的 fillMaxWidth() 需要的是比例,不是具体数值。
所以我们必须先把真实数据转成 UI 能理解的宽度比例。
Step 4.3:理解 Compose Bar Chart 为什么不用 Canvas 本周 Lab 里 Bar Chart 没有用 Canvas,而是用了:
在 Compose 里,矩形条可以直接用 Box + background() 做出来,不一定需要 Canvas。
左边显示 label;
中间显示柱子;
右边显示数值。
Step 4.4:理解 ComposePieChart 为什么需要 Canvas Pie Chart 是圆形扇区,不是普通的矩形布局。
Compose 的 Row、 Column、 Box 适合排版,但不适合直接画圆弧和扇形。
所以 Pie Chart 更适合用 Canvas 自己绘制。
Step 4.5:理解角度计算
Step 4.6:理解 startAngle 的作用 startAngle 记录当前扇形从哪里开始;
每画完一个扇形,就把起点往后移动;
这样下一个扇形就会接在前一个扇形后面。
用 -90f 可以让饼图从顶部开始画,看起来更符合常见图表习惯。
Task 5: 运行并验证 Compose 自绘图表
Step 5.1:运行 Compose 自绘版本 点击 Android Studio 顶部绿色运行按钮。
Step 5.2:切换到 Compose Bar Chart 页面显示横向 Bar Chart;
每一行包含分类名称、横向柱子、数值;
数值越大,柱子越长。
Summary 本周 Lab 的核心不是单纯“画两个图”,而是让你理解 Android 中做数据可视化的两种思路。
第一种思路是使用第三方库,例如 MPAndroidChart。这种方式适合快速做出功能完整的图表,因为库已经帮你处理了很多细节,例如坐标轴、标签、动画和图例。但是在 Compose 项目中使用传统 View 库时,需要通过 AndroidView 把它嵌入 Compose 页面。
第二种思路是使用 Jetpack Compose 自己实现图表。Bar Chart 因为本质是矩形条,所以可以直接用 Row、 Column、 Box 和 Text 组合出来。Pie Chart 因为涉及圆形扇区,所以需要用 Canvas 和 drawArc() 手动画出来。
本周必须理解的知识点
1. MPAndroidChart 是什么 MPAndroidChart 是一个第三方图表库,可以帮助 Android App 快速创建各种图表,例如:
Bar Chart
Pie Chart
Line Chart
Radar Chart
功能完整;
自带动画;
自带坐标轴;
自带标签和图例;
适合复杂图表。
2. JitPack 是什么 JitPack 是一个可以从 GitHub 项目生成依赖包的仓库服务。
3. AndroidView 是什么 AndroidView 是 Compose 提供的桥梁,用来在 Compose 页面中显示传统 Android View。
如果一个 UI 元件不是 Compose 写的,而是传统 View 写的,就可以用 AndroidView 放进 Compose。
4. Canvas 是什么 Canvas 是 Compose 中比较底层的绘图工具。
5. FilterChip 是什么 FilterChip 是 Material Design 里的选择组件。
比普通 Button 更合适,因为它可以清楚显示当前哪个模式被选中。
关键概念对比
MPAndroidChart vs Compose 自绘图表
Bar Chart vs Pie Chart
Row / Column / Box vs Canvas Gradle Scripts > settings.gradle.ktsGradle Scripts > build.gradle.kts (Module :app)gradle/libs.versions.toml[versions]
mpandroidchart = "v3.1.0"
[libraries]
mpandroidchart = {module = "com.github.PhilJay:MPAndroidChart",version.ref = "mpandroidchart" }app/src/main/java/你的包名/MainActivity.kt横轴第 1 个位置,对应 Mon,步数是 4050Data Visualisation with ChartsFood = 190
maxValue = 200
barFraction = 190 / 200 = 0.95Row
├── Text(label)
├── Box(background)
│ └── Box(coloured bar)
└── Text(value)Food = 190
Total = 675
Food percentage = 190 / 675
Food angle = 190 / 675 * 360数据
→ 转换成图表需要的比例或 Entry
→ 选择图表实现方式
→ 用 FilterChip 切换显示模式
→ 运行并验证 UI 是否正确显示maven( "https://jitpack.io" ) pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven( "https://jitpack.io" ) } } rootProject.name = "GraphLab" include( ":app" ) maven { url "https://jitpack.io" } maven( "https://jitpack.io" ) implementation( "com.github.PhilJay:MPAndroidChart:v3.1.0" ) implementation(libs.mpandroidchart) implementation( "com.github.PhilJay:MPAndroidChart:v3.1.0" ) package com.example.graphlab import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.charts.PieChart 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.data.PieData import com.github.mikephil.charting.data.PieDataSet import com.github.mikephil.charting.data.PieEntry import com.github.mikephil.charting.formatter.IndexAxisValueFormatter import com.github.mikephil.charting.formatter.ValueFormatter import com.github.mikephil.charting.utils.ColorTemplate class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Surface(modifier = Modifier.fillMaxSize()) { LibraryGraphScreen() } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun LibraryGraphScreen() { var selectedMode by remember { mutableStateOf( "Pie Chart" ) } val modes = listOf( "Pie Chart" , "Bar Chart" ) Column(modifier = Modifier.padding(16.dp)) { Spacer(modifier = Modifier.height(46.dp)) Text( text = "Data Visualisation with Charts" , style = MaterialTheme.typography.headlineSmall ) Spacer(modifier = Modifier.height(12.dp)) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { modes.forEach { mode -> FilterChip( selected = selectedMode == mode, onClick = { selectedMode = mode }, label = { Text( text = mode, color = if (selectedMode == mode) Color.White else Color.Black ) }, colors = FilterChipDefaults.filterChipColors( selectedContainerColor = Color(0xFF5A5D9D), containerColor = Color(0xFFE0E0E0) ) ) } } Spacer(modifier = Modifier.height(16.dp)) when (selectedMode) { "Pie Chart" -> LibraryPieChartScreen() "Bar Chart" -> LibraryBarChartScreen() } } } @Composable fun LibraryBarChartScreen() { val barEntries = listOf( BarEntry(0f, 1070f), BarEntry(1f, 4050f), BarEntry(2f, 3890f), BarEntry(3f, 5599f), BarEntry(4f, 2300f), BarEntry(5f, 4055f), BarEntry(6f, 3200f) ) val barDataSet = BarDataSet(barEntries, "Steps" ) barDataSet.colors = ColorTemplate.COLORFUL_COLORS.toList() val barData = BarData(barDataSet) barData.barWidth = 0.8f AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> BarChart(context).apply { data = barData description.isEnabled = false setFitBars(true) xAxis.position = XAxis.XAxisPosition.BOTTOM xAxis.valueFormatter = IndexAxisValueFormatter( listOf( "Sun" , "Mon" , "Tues" , "Wed" , "Thurs" , "Fri" , "Sat" ) ) xAxis.granularity = 1f xAxis.setDrawGridLines(false) axisRight.isEnabled = false animateY(4000) invalidate() } } ) } @Composable fun LibraryPieChartScreen() { val pieEntries = listOf( PieEntry(35f, "Bills" ), PieEntry(50f, "Mortgage" ), PieEntry(5f, "Petrol" ), PieEntry(10f, "Food" ) ) val pieDataSet = PieDataSet(pieEntries, "Pie Data Set" ) pieDataSet.colors = ColorTemplate.COLORFUL_COLORS.toList() pieDataSet.xValuePosition = PieDataSet.ValuePosition.INSIDE_SLICE pieDataSet.yValuePosition = PieDataSet.ValuePosition.INSIDE_SLICE pieDataSet.valueFormatter = PercentValueFormatter() pieDataSet.valueTextSize = 14f val pieData = PieData(pieDataSet) AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> PieChart(context).apply { data = pieData description.isEnabled = false centerText = "Expenses" setDrawCenterText(true) setEntryLabelTextSize(14f) setUsePercentValues(false) animateY(4000) invalidate() } } ) } class PercentValueFormatter : ValueFormatter() { override fun getFormattedValue( value : Float): String { return " ${ value.toInt() } %" } } var selectedMode by remember { mutableStateOf( "Pie Chart" ) } val modes = listOf( "Pie Chart" , "Bar Chart" ) FilterChip( selected = selectedMode == mode, onClick = { selectedMode = mode }, label = { Text(text = mode) } ) val barEntries = listOf( BarEntry(0f, 1070f), BarEntry(1f, 4050f), BarEntry(2f, 3890f), BarEntry(3f, 5599f), BarEntry(4f, 2300f), BarEntry(5f, 4055f), BarEntry(6f, 3200f) ) xAxis.valueFormatter = IndexAxisValueFormatter( listOf( "Sun" , "Mon" , "Tues" , "Wed" , "Thurs" , "Fri" , "Sat" ) ) AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> BarChart(context).apply { data = barData } } ) val pieEntries = listOf( PieEntry(35f, "Bills" ), PieEntry(50f, "Mortgage" ), PieEntry(5f, "Petrol" ), PieEntry(10f, "Food" ) ) class PercentValueFormatter : ValueFormatter() { override fun getFormattedValue( value : Float): String { return " ${ value.toInt() } %" } } ComposeBarChart ComposePieChart ComposeGraphScreen package com.example.graphlab import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import java.util.Locale class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Surface(modifier = Modifier.fillMaxSize()) { ComposeGraphScreen() } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun ComposeGraphScreen() { var selectedMode by remember { mutableStateOf( "Pie Chart" ) } val modes = listOf( "Pie Chart" , "Bar Chart" ) val data = mapOf( "Food" to 190.0, "Transport" to 80.0, "Shopping" to 170.0, "Entertainment" to 75.0, "Other" to 160.0 ) Column(modifier = Modifier.padding(16.dp)) { Spacer(modifier = Modifier.height(16.dp)) Text( text = "Spending Analysis" , style = MaterialTheme.typography.headlineSmall ) Spacer(modifier = Modifier.height(12.dp)) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { modes.forEach { mode -> FilterChip( selected = selectedMode == mode, onClick = { selectedMode = mode }, label = { Text( text = mode, color = if (selectedMode == mode) Color.White else Color.Black ) }, colors = FilterChipDefaults.filterChipColors( selectedContainerColor = Color(0xFF5A5D9D), containerColor = Color(0xFFE0E0E0) ) ) } } Spacer(modifier = Modifier.height(16.dp)) when (selectedMode) { "Pie Chart" -> ComposePieChart( data ) "Bar Chart" -> ComposeBarChart( data ) } } } @Composable fun ComposeBarChart( data : Map<String, Double>) { if ( data .isEmpty()) return val maxValue = 200.0 val colors = listOf( Color(0xFF5A5D9D), Color(0xFF42A5F5), Color(0xFFFFCA28), Color(0xFF66BB6A), Color(0xFFAB47BC) ) val labels = data .keys.toList() val values = data .values.toList() Column( modifier = Modifier .fillMaxWidth() .padding(end = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { labels.forEachIndexed { index, label -> val barFraction = (values[index] / maxValue).toFloat().coerceIn(0f, 1f) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth() ) { Text( text = label, modifier = Modifier.widthIn(min = 90.dp, max = 160.dp), fontSize = 14.sp, maxLines = 1 ) Box( modifier = Modifier .weight(1f) .height(24.dp) .background(Color.LightGray) ) { Box( modifier = Modifier .fillMaxWidth(barFraction) .fillMaxHeight() .background(colors[index % colors.size]) ) } Text( text = values[index].toInt().toString(), modifier = Modifier.width(50.dp), fontSize = 14.sp ) } } } } @Composable fun ComposePieChart( data : Map<String, Double>) { if ( data .isEmpty()) return val total = data .values.sum() val colors = listOf( Color(0xFF5A5D9D), Color(0xFF42A5F5), Color(0xFFFFCA28), Color(0xFF66BB6A), Color(0xFFAB47BC) ) val labels = data .keys.toList() val values = data .values.toList() val colorMap = labels.mapIndexed { index, label -> label to colors[index % colors.size] }.toMap() Column(horizontalAlignment = Alignment.CenterHorizontally) { Box(modifier = Modifier.size(240.dp)) { Canvas(modifier = Modifier.fillMaxSize()) { var startAngle = -90f labels.forEach { label -> val value = data [label] ?: 0.0 val sweepAngle = ( value / total * 360f).toFloat() drawArc( color = colorMap[label] ?: Color.Gray, startAngle = startAngle, sweepAngle = sweepAngle, useCenter = true ) startAngle += sweepAngle } } } Spacer(modifier = Modifier.height(16.dp)) Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { labels.forEachIndexed { index, label -> val value = values[index] val percent = value / total * 100 Row(verticalAlignment = Alignment.CenterVertically) { Box( modifier = Modifier .size(12.dp) .background(colorMap[label] ?: Color.Gray) ) Spacer(modifier = Modifier.width(8.dp)) Text( text = "$label: ${ value.toInt() } ( ${ String.format(Locale.ENGLISH, "%.1f", percent) } %)" , fontSize = 14.sp, color = Color.Black ) } } } } } val barFraction = (values[index] / maxValue).toFloat().coerceIn(0f, 1f) drawArc( color = colorMap[label] ?: Color.Gray, startAngle = startAngle, sweepAngle = sweepAngle, useCenter = true ) val sweepAngle = ( value / total * 360f).toFloat() maven( "https://jitpack.io" ) implementation( "com.github.PhilJay:MPAndroidChart:v3.1.0" ) AndroidView( factory = { context -> BarChart(context) } )