简介:面向Android开发者的MPAndroidChart滑动柱状图实战工程,聚焦图表在有限屏幕内展示大量分类数据时的横向浏览需求。工程围绕柱状图拖拽、缩放与视口范围控制展开,涉及手势监听、X轴显示区间调整以及按可视范围动态加载数据等实现思路,适合已掌握Android基础、希望进阶图表交互与性能优化的开发者参考。压缩包共27个文件,约550KB,以xml布局与资源、java业务源码、gradle构建脚本为主体,并包含png效果预览图与jar依赖库,目录按标准Android工程组织,便于导入后对照阅读。目前已有1826人学习。读者可从中获得一套可运行的图表交互样例,用于理解滑动柱状图的手势处理、数据分页与渲染性能控制,也可作为既有项目图表模块改造的参考起点。
1. 一块 5 英寸屏塞进 30 根柱子:滑动的柱状图该在什么时候上
一块 5 英寸屏要塞进 30 个门店的月度销量,柱子被压成 3px 宽的牙签,X 轴标签叠成一团黑块,手指点上去连自己点的是哪根都分不清。改字号、调边距、缩图例,全都不解决问题——症结不在样式,在可视区间。MPAndroidChart 的 BarChart 允许把数据铺到画布之外,用手指拖拽把视口挪过去,这就是常说的滑动的柱状图。它适合分类维度超过 10 个、需要横向看趋势、又不愿意拆成多个分页 Tab 的看板场景;几十到两三百根柱子是它最舒服的区间,再往上就得先算一算绘制开销。下面从视口机制一路拆到参数、手势冲突和翻车点。
2. 滑动柱状图的底层机制:视口、缩放与触摸分发
2.1 三种「滑动」不是一回事
很多人第一次接 MPAndroidChart 会把这里的滑动和 RecyclerView 的滚动混为一谈,结果参数调了半天方向都是错的。RecyclerView 是内容位移,子 View 复用回收;BarChart 是视口位移,所有 BarEntry 一次性画在一张放大的画布上,ViewPortHandler拿着一组 scale 和 translate 值决定哪一段落在屏幕里。手指拖动改变的是这个变换矩阵,不是数据。
理解这一点之后,几个反直觉的现象就说得通了:柱子越多,滑动并不会像列表那样「懒加载」,而是全量绘制后被裁掉;setVisibleXRangeMaximum调整的是视口宽度,不是数据量;你没法通过滚动回调去「加载下一页」,因为压根没有分页这回事。所以选型第一问不是「能不能滑」,而是「柱子总数控制在多少」——我一般把 300 根当作单图的上限,超过就做聚合或切换成折线。
第二层区分是双指缩放。默认开启缩放时,用户两根手指一捏,可见区间从 7 根变成 3 根,柱宽跟着变;这和拖拽共用同一个 scaleX,只是改变的方向相反。第三层是外层容器滚动,ViewPager 或 RecyclerView 也想吃掉横向手势,这才是真正需要写代码对抗的部分,后面单独开一章。
2.2 可见区间、缩放系数与最小缩放的关系
setVisibleXRangeMaximum(float)的作用是把视口缩到「最多同时显示 N 个 X 轴单位」。X 轴单位不是像素,是 BarEntry 的 x 值间距;如果你的数据是 0、1、2……29,那么传 7 就表示屏幕上大约看到 7 根柱子的宽度。
它内部做的事可以粗略理解为:用「数据 X 轴总跨度 ÷ N」算出目标 scaleX,再把这个 scale 写进 ViewPortHandler。所以有两个连带结论。其一,柱子总数变化时可见根数不变,但单根柱子的像素宽度会变,30 根看 7 根和 300 根看 7 根,柱子视觉宽度差一个数量级。其二,如果同时调了setVisibleXRangeMinimum,用户捏合时会被卡在下限,不会缩到只剩半根柱子——这个下限建议设成 3,留出对比语境,缩到 1 根其实没有分析价值。
与之配套的是setScaleMinima(scaleX, scaleY),它限制的是整体最小缩放,和可见区间是两套东西,混用会出现「明明设了 visibleXRange 却能缩成一条线」的怪现象。我的习惯是只用setVisibleXRangeMaximum/Minimum这一对表达业务意图,setScaleMinima只在确实需要控制整体缩放时碰。
2.3 依赖接入与最小可运行骨架
库走 JitPack,根工程的仓库声明里补一行,模块里加依赖。版本锁定在 3.1.x,3.0.x 和 3.1.x 在格式化器接口上有破坏性变更,团队内必须统一。
// 根 build.gradle 的 allprojects.repositories 里加 maven { url 'https://jitpack.io' } // app/build.gradle dependencies { // v3.1.x 是当前主线,3.0.x 的 IAxisValueFormatter 在 3.1 已换成 ValueFormatter implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' }布局里没什么花活,一个占满宽度的 BarChart 即可,注意别在 XML 里给死高度又套wrap_content,图表需要确定的测量高度才能算视口。
<com.github.mikephil.charting.charts.BarChart android:id="@+id/barChart" android:layout_width="match_parent" android:layout_height="220dp" android:layout_marginStart="12dp" android:layout_marginEnd="12dp" />最小骨架的关键顺序是:先拿数据、再 setData、最后设视口。顺序错乱是新手最常见的翻车点之一,因为setData会触发一次内部重算,把你之前设的可见区间覆盖掉。
val chart = findViewById<BarChart>(R.id.barChart) val entries = rawList.mapIndexed { i, v -> BarEntry(i.toFloat(), v) } val set = BarDataSet(entries, "月销量").apply { color = 0xFF3B82F6.toInt() setDrawValues(false) // 30 根柱子下先关数值标签,减少重叠 } val data = BarData(set).apply { barWidth = 0.6f // v3 的柱宽挂在 BarData 上,不在 BarDataSet 上 } chart.data = data chart.setVisibleXRangeMaximum(7f) // 必须在 setData 之后 chart.invalidate()barWidth这个参数是 v3 里改动最容易被忽略的一处:老教程还在写barDataSet.setBarWidth(0.7f),3.1 里那方法已经没了,柱宽改为 BarData 级别统一控制,因为分组柱状图需要按组算间距。setDrawValues(false)则是性能取舍,30 根柱子上全标数字会糊成一片,需要精确值时走高亮浮层更合适。
3. 让柱子真正滑起来:从数据到视口的参数配置
3.1 数据侧:BarEntry 的 x 值必须连续且从 0 开始
滑动柱状图对 x 值的连续性非常敏感。XAxis 的标签是按浮点值去查的,如果你的 x 是时间戳或者有空洞的编号,柱子间距会忽宽忽窄,滑动时视觉上像在抽搐。稳妥做法是把所有分类映射成 0、1、2……的索引,原始标签另存一个数组,用格式化器反查。
// 原始分类名和数值分开存,索引做 x,标签做展示 data class MonthSales(val label: String, val value: Float) fun buildEntries(list: List<MonthSales>): Pair<List<BarEntry>, List<String>> { val entries = ArrayList<BarEntry>(list.size) val labels = ArrayList<String>(list.size) list.forEachIndexed { index, item -> entries.add(BarEntry(index.toFloat(), item.value)) labels.add(item.label) } return entries to labels }这段没有难度,但它是后面所有对齐、吸附、高亮定位的基础。索引连续保证了第 n 根柱子的中心永远落在 x = n 上,视口宽度 7 就真的对应 7 个完整分类,而不是 6.3 个半截柱子——后者在 visualXRange 打日志时会让你怀疑人生。
X 轴本体还需要三处配置:granularity = 1f关掉小数刻度,setAvoidFirstLastClipping(true)防止首尾标签被裁,setDrawGridLines(false)让横向滑动的画面干净一些。标签格式化用 v3.1 的ValueFormatter:
val xAxis = chart.xAxis xAxis.position = XAxis.XAxisPosition.BOTTOM xAxis.granularity = 1f xAxis.isGranularityEnabled = true xAxis.setDrawGridLines(false) xAxis.setAvoidFirstLastClipping(true) xAxis.valueFormatter = object : ValueFormatter() { override fun getFormattedValue(value: Float): String { // 视口边界会传进 6.999 这类浮点,四舍五入再查表 val idx = Math.round(value) return if (idx in labels.indices) labels[idx] else "" } }Math.round这一行是血泪经验:横向滑动时视口左边界常常停在 6.4 这种位置,直接value.toInt()会得到 6,看起来没问题,但右边界 13.6 会被截成 13,两头标签对不上,用户滑到一半看见空标签就以为数据丢了。
3.2 视口侧:五个必须同时调的方法
只调一个setVisibleXRangeMaximum是滑不动的,因为它只定义了「看多宽」,没定义「能不能动」。完整的组合是下面五个,缺一个体验就残。
chart.apply { setTouchEnabled(true) // 总开关,false 时下面全失效 isDragEnabled = true // 单指拖拽平移 setScaleEnabled(true) // 双指缩放总开关 isPinchZoom = false // 只允许双指,防止单击误触缩放 setVisibleXRangeMaximum(7f) // 视口最多 7 个 X 单位 setVisibleXRangeMinimum(3f) // 捏合下限 isFitBars = true // 让柱子中心对齐刻度,而不是骑在轴线上 isDoubleTapToZoomEnabled = false // 双击缩放对看板是干扰 setDragDecelerationEnabled(true) // 松手后的惯性 }isFitBars = true这一行值得单独说。不设它的时候,第一根柱子会以左边缘为起点向外展开,视觉上整排柱子是「骑」在轴线上的,和网格对不齐;设了之后绘制时会自动补半个柱宽的偏移量,柱子中心正好压在刻度线上。很多「图怎么感觉歪了半格」的反馈,最后都追到这个布尔值上。
isPinchZoom = false则是个产品层面的选择。允许双指缩放意味着用户能把 7 根缩成 3 根也能拉成 20 根,一旦拉到 20 根,柱子又回到牙签状态,等于把前面所有努力还回去了。我的默认配置是关闭捏合,只留拖拽,需要看更细粒度就点进去看详情页。
isDoubleTapToZoomEnabled默认是 true,在手机上看板里双击往往被用户当成「刷新」,很容易误触,建议关掉。
3.3 初始位置、惯性系数与吸附对齐
数据设完、视口设完,图表默认停在最左边,也就是最早的那几个月。如果业务是「看最近 7 天」,用户打开就得先滑一下,这个体验很糟糕。初始位置应该在布局完成后再挪:
chart.post { // 布局完成后视口才有真实宽度,这时挪动才准确 val target = (entries.size - 7).coerceAtLeast(0).toFloat() chart.moveViewToX(target) }post不是可有可无的。View 还没测量完时moveViewToX拿到的视口宽度是 0,换算出来的位置会偏出去一大截,表现就是「一进页面图是空的,滑一下才出来」。这个坑我踩过不止一次,后来一律加 post,或者放到setData之后的onLayout阶段。
惯性滑动的快慢由setDragDecelerationFrictionCoef(float)控制,取值 0 到 1,越大滑得越远,默认 0.9。做「一屏一屏翻」的卡片式看板时,我会把它调到 0.85 左右,再配一个OnChartGestureListener,在手指抬起后把视口吸附到整数边界:
chart.setDragDecelerationFrictionCoef(0.85f) chart.setOnChartGestureListener(object : OnChartGestureListener { // 其余手势回调留空,只关心抬起 override fun onChartGestureEnd(me: MotionEvent?, lastPerformedGesture: ChartGesture?) { val left = chart.lowestVisibleX val snapped = Math.round(left).toFloat() // 吸附到整根柱子 chart.moveViewToX(snapped) // 带惯性收尾的平移 } override fun onChartGestureStart(me: MotionEvent?, lastPerformedGesture: ChartGesture?) = Unit override fun onChartLongPressed(me: MotionEvent?) = Unit override fun onChartDoubleTapped(me: MotionEvent?) = Unit override fun onChartSingleTapped(me: MotionEvent?) = Unit override fun onChartFling(me1: MotionEvent?, me2: MotionEvent?, velocityX: Float, velocityY: Float) = Unit override fun onChartScale(me: MotionEvent?, scaleX: Float, scaleY: Float) = Unit override fun onChartTranslate(me: MotionEvent?, dX: Float, dY: Float) = Unit })监听器接口方法多,全实现一遍是为了编译通过,真正有用的只有onChartGestureEnd。吸附这件事在小屏上收益很明显:不吸附时用户经常停在第 6.3 根的位置,一根柱子半截露在边上,看着别扭;吸附之后每次停下都是整齐的 7 根,读起来舒服得多。
3.4 柱子数量上去之后的性能取舍
30 根和 300 根是两个世界。绘制开销主要来自三块:柱体本身、数值标签、高亮十字线。柱体是必须的,后两个可以砍。
setDrawValues(false)关数值标签,这是最立竿见影的一刀。如果产品坚持要显示数字,用setMaxVisibleValueCount(7)限制同时绘制的标签数量——注意这个方法控制的是数值标签的绘制数量,不是可见柱子数,两者经常被搞混,调错了会以为图表没反应。
setDrawBarShadow(false)关掉柱子背后的灰色投影层,省一次全量绘制。setHighlightPerDragEnabled(false)关掉拖动时的高亮跟随,只在点击时高亮,能减少大量无效重绘。再加上setDrawGridBackground(false)和setDrawBorders(false),一张 300 根柱子的图在中端机上滑起来基本能稳在 55fps 以上。
还有一处隐蔽开销是 X 轴标签:柱子越多,标签越密,setLabelCount(n, false)让库自己决定抽稀,别硬塞固定数量,否则会出现标签重叠或者大片留白。
4. 手势冲突排查:柱状图和 ViewPager、RecyclerView 抢手势怎么办
4.1 横向 ViewPager 里的横向滑动图
这是最典型的死锁现场:外层 ViewPager 想左右翻页,里面的 BarChart 想左右平移,两者抢同一个 ACTION_MOVE。默认结果是 ViewPager 赢——因为父容器在onInterceptTouchEvent里先一步拦截,图表从头到尾收不到完整的移动事件,表现就是「图不动,页面翻走了」。
解法是在图表按下时立刻向父容器声明「这个手势归我」。注意不能一按下就永久禁止拦截,那样垂直方向的页面滚动也会被吃掉;正确做法是按方向判断,横向前进时才锁。
class ScrollableBarChart @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : BarChart(context, attrs, defStyle) { private var downX = 0f private var downY = 0f private val slop by lazy { ViewConfiguration.get(context).scaledTouchSlop } override fun onTouchEvent(event: MotionEvent): Boolean { when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { downX = event.x downY = event.y // 按下时先放手,给父容器判断垂直滚动的机会 parent?.requestDisallowInterceptTouchEvent(false) } MotionEvent.ACTION_MOVE -> { val dx = abs(event.x - downX) val dy = abs(event.y - downY) if (dx > dy && dx > slop) { // 确认是横向手势,锁死父容器,直到抬手 parent?.requestDisallowInterceptTouchEvent(true) } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { // 一定要复位,否则下一次垂直滚动会失效 parent?.requestDisallowInterceptTouchEvent(false) } } return super.onTouchEvent(event) } }scaledTouchSlop那层判断不能省。手指刚按下时会有几像素抖动,如果第一帧就判定为横向,轻微的上下滑动也会被误锁,用户会觉得列表卡住了。加上 slop 阈值之后,只有明确越过阈值的横向位移才触发锁定。
ACTION_UP和ACTION_CANCEL的复位同样关键。漏掉 CANCEL 分支是常见疏漏:手势被系统打断(比如来电、弹窗)时只走 CANCEL 不走 UP,父容器就一直处于被禁止拦截的状态,后续整页都滑不动,只能杀掉页面重进。
4.2 垂直 RecyclerView 列表里嵌横向图表
这个场景反过来:外层是垂直滚动,内层是横向滑动,冲突概率低一些,但仍有坑。RecyclerView 的onInterceptTouchEvent在检测到垂直位移后会接管事件,如果你的图表高度比较大、用户斜着划,很容易划到一半图表失去响应。
第一种解法还是上面那个自定义 View,直接替换 XML 里的类名,一行布局改动。第二种是给 RecyclerView 设isNestedScrollingEnabled = false,但这是杀敌一千自损八百,除非确认列表很短,否则别用。
还有一处容易忽略:RecyclerView 复用时,图表的视口位置会被带到新的 item 上。列表滑出屏幕再滑回来,图停在上次的位置,但数据可能已经换了一批,看起来就是「图乱跳」。解决办法是在onViewRecycled或者绑定时重置视口:
override fun onBindViewHolder(holder: VH, position: Int) { holder.chart.data = buildData(items[position]) holder.chart.setVisibleXRangeMaximum(7f) holder.chart.moveViewToX(0f) // 每次绑定回到起点 holder.chart.invalidate() }4.3 高亮、长按与拖动的三角关系
高亮本身不是手势冲突,但它会和拖动抢响应。默认setHighlightPerDragEnabled(true),拖动过程中高亮框跟着手指跑,每一次移动都触发一次高亮重绘,柱子多的时候掉帧明显。如果你的看板不需要「拖动时实时显示数值」,关掉它,只在点击时高亮。
另外setMaxHighlightDistance决定手指离柱子多近才算命中,默认 500dp,实际偏大,密集柱状图里会出现「点空白也高亮」的情况,调到 60 到 100dp 之间更符合直觉。高亮内容建议用MarkerView自定义气泡,别用默认的十字线——30 根柱子里十字线会盖住邻柱,读数反而更难。
5. 避坑清单:滑动柱状图的五类翻车现场
5.1 设了 setVisibleXRangeMaximum,图还是压成一团
现象:代码里明明写了setVisibleXRangeMaximum(7f),运行起来 30 根柱子还是全部挤在屏幕上,一根都没被切掉。
原因基本锁定在调用顺序上。这个方法的内部逻辑是修改视口缩放,而setData会触发一次完整的数据重算,把视口重置回「适应全部数据」。如果你的调用写在setData之前,等于白设。另一种情况是在onCreate里调,此时 View 还没测量,宽度是 0,换算出来的缩放比无意义。
解决办法是把视口配置挪到setData之后,并确保在布局完成后再做需要像素宽度的操作。稳妥的写法是setData→setVisibleXRangeMaximum→post { moveViewToX(...) }→invalidate,四步顺序别乱。如果仍然无效,检查一下是不是在别处又调了fitScreen()或者zoomOut(),这两招会直接覆盖视口。
5.2 滑动之后柱子「走位」,和 X 轴刻度错开半格
现象:静止时看着还行,一滑动就发现柱子中心不在刻度线正下方,越滑偏得越明显,两边留白也不对称。
这是isFitBars没开。默认值 false 时,绘制逻辑以 x=0 为柱子的左边缘起点,整排柱子相对刻度整体偏移半个柱宽,静止时不太看得清,滑动时因为视口边界在动,偏移量被放大成肉眼可见的错位。
另一个次要原因是barWidth设得过大。柱宽超过 1.0 会让相邻柱子互相覆盖,视觉上像是「少了一根」。分组柱状图里这个值还要再除以组内数量,比如三组并排,barWidth建议 0.3 左右,具体靠试。修完之后记得invalidate(),光改数据不刷新是看不到效果的。
5.3 一群标签重叠成黑块,或者出现 1.9999 这种鬼值
现象:X 轴下方标签密密麻麻叠在一起,或者某几根柱子的标签显示成2.0001、6.999。
前者是标签密度没控制。柱子多、可见区间又被限制在 7 根时,库会尝试给每个可见刻度都画标签,如果你的 formatter 返回空串的比例不对,就会出现「有的有、有的没有、有的重叠」。用setLabelCount(n, false)让库自己抽稀,配合 formatter 里对越界索引返回空串,通常能干净。
后者是浮点误差没处理。视口边界传进 formatter 的值天然带小数,直接强转toInt()在 6.999 上会得到 6 而 7.0 得到 7,同一个位置两次滑动显示不同标签。统一走Math.round(value)再查表,问题消失。这类问题在测试机上不一定复现,跟屏幕密度和具体滑动位置有关,纯属玄学,但日志里打个lowestVisibleX就能看清。
5.4 放进 ViewPager 之后完全滑不动,页面倒是翻得飞快
现象:单独跑 Demo 一切正常,嵌进 ViewPager 后图表变成一张静态图,手指在上面划只会翻页。
父容器抢手势,前面已经拆过原理。这里补充两个排查细节:一是确认自定义 View 的类名确实写进了 XML,有时候改完 Kotlin 文件忘了改布局,跑起来还是原生 BarChart;二是确认super.onTouchEvent(event)被调用到了,返回 false 的话图表自己都不处理事件,再怎么请求父容器不拦截也没用。
还有一种隐蔽情况:图表被包在NestedScrollView里。NestedScrollView 的拦截逻辑和普通 ViewGroup 不同,requestDisallowInterceptTouchEvent在部分版本上要配合isNestedScrollingEnabled = false才生效。这类嵌套能避就避,图表给固定高度直接放在 LinearLayout 里最省心。
5.5 数据更新后图不刷新,或者刷新后视口跳回原点
现象:切换月份、重新拉数据,调了invalidate()但图上还是老数据;或者数据变了,图的位置也莫名回到了最左边。
第一种是没走notifyDataSetChanged。MPAndroidChart 的刷新分两层:数据层变更要调barData.notifyDataSetChanged(),视图层要调chart.invalidate(),只调后者在部分场景下不会重建内部缓存。第二种是视口被重算覆盖,setData之后必须重新设一次setVisibleXRangeMaximum和初始位置,不然就回到默认的「显示全部」。
如果数据量变化很大(比如从 12 根变成 120 根),还要顺手清一下高亮,chart.highlightValue(null),否则高亮框会留在已经不存在的索引上,表现为图上有个飘着的空框。
6. 进阶:把滑动柱状图接进真实看板的三处细节
做到能滑只是及格线,真正上线还要处理三件事:分组柱子的视口换算、滑动手感对齐、以及怎么验证视口真的按预期工作。
先说分组。当每根「柱子」其实是一组并排的三根(比如三个渠道的数据),视口单位就不再是单根柱子,而是一组。此时setVisibleXRangeMaximum(7f)表示看到 7 组,柱宽要除以组数,X 轴标签仍然按组索引格式化。常见错误是按单柱去算可见区间,结果一屏显示 21 根细柱,标签直接消失。
| 参数 | 作用对象 | 单柱图常用值 | 三柱分组常用值 | 设错的后果 |
|---|---|---|---|---|
barWidth | BarData 级 | 0.6 ~ 0.7 | 0.25 ~ 0.3 | 过大互相覆盖,过小柱间留白 |
setVisibleXRangeMaximum | 视口 | 7 | 7(组数) | 按单柱算会显示过密 |
granularity | XAxis | 1f | 1f | 关掉会出现小数刻度 |
setMaxVisibleValueCount | 数值标签 | 7 ~ 10 | 3 ~ 5 | 与可见柱数无关,调错无感 |
setDragDecelerationFrictionCoef | 惯性 | 0.85 | 0.85 | 越大滑越远,过小像卡住 |
再说手感对齐。前面提到的吸附只按左边界取整,当可视区间是 7.5 这种非整数时,左右两边会各露半根柱子。更稳的做法是固定setVisibleXRangeMaximum为整数,吸附时用Math.round(lowestVisibleX),同时确保setVisibleXRangeMinimum不小于 3,这样捏合后的边界也不会出现半格。
最后是验证。视口这种黑匣子,光靠眼睛看容易自我说服,我习惯在滑动结束时打日志对账:
chart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { override fun onValueSelected(e: Entry?, h: Highlight?) { // 点击时顺带核对视口边界是否在合法区间 val left = chart.lowestVisibleX val right = chart.highestVisibleX Log.d("ChartViewPort", "left=$left right=$right span=${right - left}") // 期望:span 约等于 7,且 0 <= left < right <= entryCount } override fun onNothingSelected() = Unit })日志里三个值要同时成立:span稳定在 7 附近(浮动 0.2 以内属正常)、left不小于 0、right不超过柱子总数。如果span变成 30,说明视口没生效;如果left出现负数,说明moveViewToX传了越界值,库内部做了钳制但动画会显得一顿。这套对账我一般只在调试包打开,正式包关掉,日志本身不影响性能但不该留。
最容易被忽略的是数据切换后的视口一致性。做过一个跨月对比的看板,用户切到 2 月(28 天)再切回 1 月(31 天),视口停留在上次的绝对位置,导致 31 天那张图右边空出一截。后来统一改成切数据时按比例复位:moveViewToX(entryCount * lastRatio),lastRatio记录的是切换前的lowestVisibleX / 旧的 entryCount,这样用户视角的「相对位置」保持一致,切换体验连贯很多。
那次之后我每次接滑动柱状图都强制走一遍固定流程:setData → 设可见区间 → post 里挪初始位置 → 打日志核对 lowestVisibleX 和 highestVisibleX → 再嵌进外层容器测手势。跳过任何一步,后面都要花两三倍时间回来找。希望帮到你。
本文还有配套的精品资源,点击获取