
简介这份资源面向需要在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. 项目骨架与依赖从零搭一个可滑动的柱状图 Demo2.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 version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp com.github.mikephil.charting.charts.BarChart android:idid/barChart android:layout_widthmatch_parent android:layout_height300dp / /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. 填充数据 ListBarEntry 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 轴往往是日期或类别名。这时候需要自定义ValueFormatterfinal 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:hardwareAcceleratedtrue。这个默认是开的但有些老项目会手动关掉。如果还卡就得考虑分页加载了。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 封装一个可复用的 BarChartViewDemo 里的配置代码全堆在 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(ListBarEntry 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); } }这样在布局里直接用BarChartViewActivity 里只需要调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不管数据量有没有变。这个习惯帮我省了不少后悔药。希望帮到你。本文还有配套的精品资源点击获取