简介:这份资源面向需要在Android应用中展示大量数据的开发者,聚焦用MPAndroidChart实现可滑动的柱状图这一实用技巧。MPAndroidChart本身并未直接提供滚动功能,但通过开启图表的拖拽与缩放能力,配合手势监听动态调整缩放级别和视口中心点,即可模拟出流畅的横向滑动效果,同时结合X轴格式化器按当前显示范围动态加载数据,兼顾性能与体验。资源包共27个文件,以xml布局、java源码、gradle构建脚本为主,另含png截图、jar依赖及proguard混淆配置等,压缩包约550KB,结构完整可直接参考。目前已有1824人学习下载,适合希望掌握图表交互与大数据量展示方案的中高级Android开发者研读借鉴。
1. 滑动的柱状图:MPAndroidChart 在真实项目里到底怎么落地
去年做一个运动健康类的 Android 项目,产品经理拿着竞品截图过来说:“这个柱状图能左右滑,数据多的时候也不卡,你搞一下。”我当时第一反应是自定义 View 从头画,后来发现时间根本不够,于是翻出了 MPAndroidChart 这个老牌图表库。它内置的HorizontalScrollBar和手势缩放机制,配合BarChart的setVisibleXRangeMaximum,几行代码就能让柱状图在数据量超过屏幕宽度时自然横向滚动。这个项目适合两类人:一是刚接触 Android 图表、需要快速出效果的新手;二是已经用过 MPAndroidChart 但被滑动卡顿、数据错位坑过的熟手。下面我把从依赖引入到滑动调优的完整链路拆开讲,代码可以直接抄。
2. 项目骨架与依赖:从零搭一个可滑动的柱状图 Demo
2.1 技术选型与工程结构
这个 Demo 的技术栈很纯粹:Android 原生 + MPAndroidChart。没有用 Jetpack Compose,因为 MPAndroidChart 本质是一个自定义 ViewGroup,在 XML 里声明最省事。工程目录结构如下:
app/ ├── src/main/java/com/example/barchartdemo/ │ ├── MainActivity.java // 入口,负责初始化图表和数据 │ ├── ChartConfig.java // 图表样式配置的集中管理类 │ └── BarDataProvider.java // 模拟数据生成,方便替换成真实接口 ├── src/main/res/layout/ │ └── activity_main.xml // 放置 BarChart 的布局文件 └── build.gradle // 依赖声明把配置和数据生成拆成独立类,是为了后面改样式或换数据源时不用动 Activity。常见做法是再抽一个BaseChartActivity,但 Demo 阶段没必要过度设计。
2.2 引入依赖与布局声明
在app/build.gradle的 dependencies 里加一行:
dependencies { implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' // 其他依赖省略 }如果拉不下来,在项目根目录的settings.gradle里确认maven { url 'https://jitpack.io' }已经加上。这是 MPAndroidChart 唯一的官方分发渠道,版本号写v3.1.0是当前稳定版,别用v3.0.3,那个版本在 Android 12 上有触摸事件兼容问题。
布局文件activity_main.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <com.github.mikephil.charting.charts.BarChart android:id="@+id/barChart" android:layout_width="match_parent" android:layout_height="300dp" /> </LinearLayout>高度必须给固定值或0dp配合 weight,不能写wrap_content。BarChart 内部测量逻辑对wrap_content支持不好,会变成一条细线,这是新手最容易翻车的地方。
2.3 初始化图表与滑动参数
MainActivity的onCreate里做三件事:拿控件、配样式、塞数据。
public class MainActivity extends AppCompatActivity { private BarChart barChart; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); barChart = findViewById(R.id.barChart); // 1. 基础样式配置 barChart.setDrawGridBackground(false); // 去掉网格背景 barChart.getDescription().setEnabled(false); // 隐藏右下角描述文字 barChart.getLegend().setEnabled(false); // 隐藏图例 barChart.setTouchEnabled(true); // 开启触摸 barChart.setDragEnabled(true); // 开启拖拽 barChart.setScaleEnabled(false); // 关闭双指缩放,只保留滑动 barChart.setPinchZoom(false); // 禁止捏合缩放 // 2. 关键:设置可视区域最大柱数 barChart.setVisibleXRangeMaximum(7); // 屏幕最多显示7根柱子 // 3. X轴配置 XAxis xAxis = barChart.getXAxis(); xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); xAxis.setGranularity(1f); // 最小间隔为1 xAxis.setDrawGridLines(false); // 4. Y轴配置 YAxis leftAxis = barChart.getAxisLeft(); leftAxis.setAxisMinimum(0f); // 从0开始 barChart.getAxisRight().setEnabled(false); // 隐藏右轴 // 5. 填充数据 List<BarEntry> entries = BarDataProvider.generate(30); BarDataSet dataSet = new BarDataSet(entries, "步数"); dataSet.setColor(Color.parseColor("#4CAF50")); dataSet.setValueTextSize(10f); BarData barData = new BarData(dataSet); barData.setBarWidth(0.6f); // 柱子宽度占间隔的60% barChart.setData(barData); // 6. 刷新并滑动到末尾 barChart.invalidate(); barChart.moveViewToX(entries.size() - 1); // 初始显示最新数据 } }setVisibleXRangeMaximum(7)是滑动的核心。它告诉图表:不管数据有多少条,X 轴可视范围内最多渲染 7 根柱子。超出的部分通过拖拽查看。如果不设这个值,所有柱子会挤在一起,滑动也就无从谈起。
moveViewToX(entries.size() - 1)让图表初始化后直接定位到最后一根柱子,适合展示“最近数据”的场景。如果产品要求从第一根开始看,把这行删掉或改成moveViewToX(0)。
BarDataProvider.generate(30)生成 30 条模拟数据,实际项目中替换成从 Room 或网络接口拿到的 List 即可。注意 BarEntry 的 x 值必须是递增的 float,不能有重复,否则滑动时会出现柱子重叠的玄学问题。
3. 核心模块拆解:滑动、渲染与数据绑定的完整链路
3.1 滑动机制:HorizontalScrollBar 与手势分发
MPAndroidChart 的滑动不是靠 ScrollView 套出来的,而是图表内部自己处理触摸事件。BarChart继承自BarLineChartBase,后者实现了OnTouchListener和ScaleGestureDetector。当setDragEnabled(true)时,图表在onTouchEvent的ACTION_MOVE里计算偏移量,然后调用mViewPortHandler的scroll方法移动视口。
视口(ViewPort)是理解滑动的关键。你可以把它想象成一个取景框,数据是背后的长卷轴。setVisibleXRangeMaximum(7)决定了取景框的宽度,moveViewToX决定取景框的位置。每次手指滑动,取景框在长卷轴上平移,图表只渲染框内的柱子。
这里有一个容易忽略的参数:setDragDecelerationEnabled(true)。默认是开启的,滑动松手后会有惯性减速效果。如果觉得滑得太“飘”,可以关掉它,或者用setDragDecelerationFrictionCoef(0.9f)调整摩擦系数,值越接近 1 滑得越远。
3.2 数据绑定:BarDataSet 与 XAxis 的 ValueFormatter
默认情况下,X 轴显示的是 BarEntry 的 x 值(0、1、2…)。但真实项目里 X 轴往往是日期或类别名。这时候需要自定义ValueFormatter:
final String[] labels = {"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"}; xAxis.setValueFormatter(new ValueFormatter() { @Override public String getFormattedValue(float value) { int index = (int) value; if (index >= 0 && index < labels.length) { return labels[index]; } return ""; } });注意getFormattedValue的返回值不能太长,否则 X 轴标签会重叠。如果标签确实长,用xAxis.setLabelRotationAngle(-45f)倾斜显示。
Y 轴的值格式化同理,比如加单位“步”:
leftAxis.setValueFormatter(new ValueFormatter() { @Override public String getFormattedValue(float value) { return (int) value + "步"; } });3.3 渲染优化:为什么数据多了会卡
当数据量超过 500 条时,滑动开始掉帧。原因在于 MPAndroidChart 默认会为每个 BarEntry 绘制矩形和数值文本。优化手段有三个:
第一,关闭数值文本绘制:dataSet.setDrawValues(false)。如果产品必须显示数值,改成只在选中时显示。
第二,降低柱子宽度:barData.setBarWidth(0.4f)。宽度越小,绘制面积越小。
第三,开启硬件加速:在 AndroidManifest 的 application 或 activity 标签加android:hardwareAccelerated="true"。这个默认是开的,但有些老项目会手动关掉。
如果还卡,就得考虑分页加载了。setVisibleXRangeMaximum只控制渲染范围,但数据源本身如果是一次性塞进去的,内存占用不会减少。常见做法是滑动到边缘时触发加载更多,用OnChartGestureListener监听onChartTranslate事件,判断当前视口是否接近数据末尾。
barChart.setOnChartGestureListener(new OnChartGestureListener() { @Override public void onChartTranslate(MotionEvent me, float dX, float dY) { float lowestVisibleX = barChart.getLowestVisibleX(); if (lowestVisibleX > entries.size() - 10) { // 触发加载更多 } } // 其他回调方法省略 });getLowestVisibleX()返回当前视口最左边的 x 值,用它和总数据量比较,就能判断是否该加载下一页。
4. 避坑与排查:滑动柱状图最常见的 5 个翻车现场
4.1 柱子挤在一起,滑动没反应
现象:数据有 50 条,但屏幕上所有柱子挤成一团,手指滑动没反应。
原因:没有调用setVisibleXRangeMaximum,或者调用了但值设得太大(比如设成 50),导致视口宽度等于数据总量,没有可滑动的空间。
解决:把setVisibleXRangeMaximum设成 5 到 10 之间的值,具体看屏幕宽度和柱子数量。同时确认setDragEnabled(true)和setTouchEnabled(true)都已开启。
4.2 滑动后 X 轴标签错位
现象:滑动到中间位置,X 轴显示的标签和柱子对不上,比如柱子是“3月”的数据,标签却显示“5月”。
原因:自定义ValueFormatter时,getFormattedValue收到的 float 值可能带小数(比如 2.9999),直接强转 int 会变成 2 而不是 3。这是浮点数精度问题,属于玄学范畴。
解决:在强转前加 0.5f 再取整,或者用Math.round(value):
int index = Math.round(value);4.3 初始位置不在最右边
现象:调用了moveViewToX(entries.size() - 1),但图表初始还是显示最左边的数据。
原因:moveViewToX必须在setData和invalidate之后调用,而且如果数据是异步加载的,得在数据回调里再调一次。另外,moveViewToX的参数是 x 值,不是索引,如果 BarEntry 的 x 值不是从 0 开始连续递增,传索引进去就会偏。
解决:确认 BarEntry 的 x 值从 0 开始,每次加 1。异步加载时在onResponse里调moveViewToX。
4.4 滑动到边缘时回弹太猛
现象:滑到最左边或最右边时,松手后图表猛地弹回,体验很差。
原因:setDragDecelerationFrictionCoef的值设得太小,或者setDragDecelerationEnabled没开。
解决:保持setDragDecelerationEnabled(true),把摩擦系数设在 0.85 到 0.95 之间。另外检查setScaleEnabled(false)是否误开了缩放,缩放和拖拽同时开启时边缘行为会混乱。
4.5 数据更新后滑动位置重置
现象:下拉刷新拿到新数据,调用notifyDataSetChanged后,图表跳回最左边。
原因:notifyDataSetChanged会重置视口位置。这是 MPAndroidChart 的设计行为,不是 bug。
解决:在更新数据前用barChart.getLowestVisibleX()记下当前位置,更新后再moveViewToX回去。或者用barChart.highlightValue(null)清除高亮后再更新。
5. 二次开发:把 Demo 改造成生产级图表组件
5.1 封装一个可复用的 BarChartView
Demo 里的配置代码全堆在 Activity 里,实际项目肯定不能这么干。我一般会封装一个BarChartView继承自BarChart,把常用配置在构造函数里做完:
public class BarChartView extends BarChart { public BarChartView(Context context) { super(context); init(); } public BarChartView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { setDrawGridBackground(false); getDescription().setEnabled(false); getLegend().setEnabled(false); setTouchEnabled(true); setDragEnabled(true); setScaleEnabled(false); setPinchZoom(false); setVisibleXRangeMaximum(7); setDragDecelerationEnabled(true); setDragDecelerationFrictionCoef(0.9f); XAxis xAxis = getXAxis(); xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); xAxis.setGranularity(1f); xAxis.setDrawGridLines(false); YAxis leftAxis = getAxisLeft(); leftAxis.setAxisMinimum(0f); getAxisRight().setEnabled(false); } public void setData(List<BarEntry> entries, String label) { BarDataSet dataSet = new BarDataSet(entries, label); dataSet.setColor(Color.parseColor("#4CAF50")); dataSet.setValueTextSize(10f); dataSet.setDrawValues(false); BarData barData = new BarData(dataSet); barData.setBarWidth(0.6f); setData(barData); invalidate(); moveViewToX(entries.size() - 1); } }这样在布局里直接用BarChartView,Activity 里只需要调setData。改样式时只动一个文件,不用满项目搜setDrawGridBackground。
5.2 滑动加载更多的实现细节
生产环境的数据往往分页返回。在BarChartView里加一个接口:
public interface OnLoadMoreListener { void onLoadMore(); } private OnLoadMoreListener loadMoreListener; private boolean isLoadingMore = false; public void setOnLoadMoreListener(OnLoadMoreListener listener) { this.loadMoreListener = listener; } // 在 init() 里加手势监听 setOnChartGestureListener(new OnChartGestureListener() { @Override public void onChartTranslate(MotionEvent me, float dX, float dY) { if (loadMoreListener == null || isLoadingMore) return; float lowestVisibleX = getLowestVisibleX(); if (lowestVisibleX > getData().getEntryCount() - 10) { isLoadingMore = true; loadMoreListener.onLoadMore(); } } // 其他回调留空 }); public void onLoadMoreFinished() { isLoadingMore = false; }调用方在onLoadMore里请求下一页数据,拿到后追加到 entries 列表,调notifyDataSetChanged,再调onLoadMoreFinished。注意isLoadingMore标志位必须有,否则滑动过程中会连续触发多次请求。
5.3 验证改造效果的方法
改完之后怎么确认没问题?我习惯用三个场景验证:
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 数据量 10 条 | 打开页面 | 全部显示,无滑动 |
| 数据量 100 条 | 左右滑动 | 流畅滚动,边缘不回弹异常 |
| 数据量 1000 条 | 快速滑动到末尾 | 触发加载更多,无卡顿掉帧 |
用 Android Studio 的 Profiler 看渲染帧率,滑动时保持在 60fps 左右算合格。如果掉到 30fps 以下,回去检查setDrawValues是否关了、柱子宽度是否太大。
5.4 一个我踩过的坑
有一次改完封装类,测试反馈说滑动到最右边后,再往左滑会卡住。排查了半天,发现是setVisibleXRangeMaximum在数据更新后没有重新调用。数据从 30 条变成 50 条时,视口最大范围还是按 30 条算的,导致末尾计算越界。从那以后我每次更新数据都强制走一遍setVisibleXRangeMaximum和moveViewToX,不管数据量有没有变。这个习惯帮我省了不少后悔药。
希望帮到你。
本文还有配套的精品资源,点击获取