ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Android K线图实现指南:从自绘到性能优化的完整实践

Android K线图实现指南:从自绘到性能优化的完整实践 简介面向安卓开发者与金融行情学习者的K线图实现范例以完整工程包形式梳理了K线绘制、分时图与KDJ指标等核心模块。资源共有一百五十三个文件包括五十八张图片素材、三十四个编译后的类文件、二十四个源码文件、二十一个界面布局与配置文件并附有可直接安装的安装包和调试文件总大小约四点九七兆字节。压缩包内还包含第三方依赖、运行属性配置和工程结构描述文件目录层次清晰能够从页面、片段到自定义图表控件逐层阅读源码。已有二百一十三人学习/下载适合希望快速上手安卓图表自定义绘制与股票类应用开发的初中级开发者。借助该工程可以掌握自绘图表控件的事件处理、多线程数据刷新以及图表缓存策略熟悉后也能用于课程设计或毕业设计的参考起点。 做了几年Android开发最怕听到的需求就是“做个K线图”。股票、基金、数字货币只要涉及行情展示K线图基本是跑不掉的标配。这个需求说难不难说简单也不简单网上能搜到一堆开源库但真正要接到项目里对齐需求、调样式、解决性能问题每一步都有不少坑。这篇东西就从一个实际开发者的角度把Android端K线图的实现思路、核心绘制细节、性能优化方案和常见坑位一次讲透给正在做或者准备做行情类App的朋友做个参考。1. 方案选型自己画还是用开源库K线图实现路径大致分两类一是直接用现成的开源库二是自己通过自定义View绘制。这两条路我都走过先说结论如果只是做个静态展示、不涉及复杂交互开源库完全够用但只要你后续要做分时图联动、各种指标切换、大数据量滑动就老老实实自己写绕不开的。1.1 主流开源库评测Android生态里做K线图最常见的几个库分别是MPAndroidChart、KLineChart和SmartChart。MPAndroidChart是老牌图表库社区活跃度高文档也全。它的K线图叫CandleStickChart支持蜡烛图、柱状图、折线图混合展示。但有两个明显的短板一是它的K线交互做得不够细十字光标、长按高亮、滑动跟手度都差点意思二是大量数据时性能会肉眼可见地卡毕竟内部用了大量的Canvas绘制和重绘逻辑。KLineChart是专为K线设计的轻量库支持主图指标MA、BOLL、MACD等和副图指标切换缩放滑动的手感调得不错。它的缺点是拓展性受限想改个样式或者加点自定义元素得深入源码改维护成本不低。网上不少项目直接拿它改改到后面和上游脱节升级都升不了。SmartChart是国人开源的方案支持K线、分时、指标联动性能优化做得不错在金融类App里有不少应用。它的缺点是文档相对简单遇到问题主要靠读源码对新手不太友好。1.2 为什么最终还是选择自绘我手头的项目最终选择自绘核心原因有三个。第一K线图的UI定制需求太多了。不同券商App的颜色、字体、指标参数、十字光标样式五花八门。用开源库碰到一个边角需求就得翻源码时间成本高。第二性能和内存需要精确控制。行情数据动辄几千几万个点还要支持分页加载、预加载、缓存开源库的封装隐藏了太多细节出了问题没法快速定位。第三后续要加自己的技术指标。市面上开源的指标就那么几个真到做量化策略或者特色指标时只有自己掌握绘制的每个像素才能玩得转。自绘听起来工作量很大但拆解下来核心其实就是数据管理、指标计算、Canvas绘制、触摸交互这几块每块都有清晰的实现路径下面逐个说。2. 数据结构和核心算法准备在动手画之前数据结构得先设计好。这部分出问题后面全白搭。2.1 K线实体数据模型K线图每根K线包含开盘价、收盘价、最高价、最低价四个核心价格以及对应的成交量、时间戳。对应Java类如下public class KLineBean { public long timestamp; // 时间戳毫秒 public float open; // 开盘价 public float close; // 收盘价 public float high; // 最高价 public float low; // 最低价 public float volume; // 成交量 public float amount; // 成交额可选 }这里有个经验价格类型用float就行double虽然精度高但在大量数据计算和绘制时float的性能表现更好K线图展示场景下float的精度完全够用。千万别用String存价格内存占用大计算也不方便这是新手很容易踩的坑。2.2 指标计算MA均线和成交量均线MA是K线图最基础的指标。MA5就是最近5根K线收盘价的平均值MA10、MA20以此类推。实现上可以用滑动窗口避免每次全量计算public static float[] calcMA(float[] closes, int period) { int n closes.length; float[] mas new float[n]; float sum 0f; for (int i 0; i n; i) { sum closes[i]; if (i period) sum - closes[i - period]; mas[i] sum / Math.min(i 1, period); } return mas; }成交量柱状图相对简单直接用volume值映射高度绘制矩形即可。但要注意成交量图的Y轴刻度通常和价格图分开主图用价格区间副图用成交量区间两者要独立计算Scale不能共用一套坐标映射。2.3 可见区间的管理K线图绘制不是把全部数据一次画出来而是只绘制当前屏幕上可见的部分。比如总共有5000根K线屏幕宽度360dp每根K线占8dp那么一屏大约显示45根实际需要绘制的数据只有屏幕上那45根其他数据在滑动时再动态取。这就需要维护一个startIndex和endIndex表示当前屏幕显示的数据区间public int startIndex; // 屏幕最左侧K线在全部数据中的下标 public int endIndex; // 屏幕最右侧K线在全部数据中的下标每次滑动或缩放只需要改变这两个下标然后触发重绘即可。这个思路是整个K线图性能优化的基石一定要想明白。3. 核心绘制细节蜡烛图、十字光标和指标数据准备好接下来是重头戏绘制。这一节讲的都是实际项目中验证过的绘制方案代码思路可以直接拿去做参考。3.1 蜡烛图的绘制蜡烛图是由实体和上下影线组成的。实体表示开盘到收盘的区间影线表示最高到最低的区间。涨跌颜色规则一般遵循“红涨绿跌”国内习惯或者“绿涨红跌”欧美习惯具体看产品要求。核心绘制代码如下Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 计算缩放比例K线宽度和间隔 float candleWidth mCandleWidth; for (int i mStartIndex; i mEndIndex; i) { KLineBean bean mData.get(i); float x getX(i); // 根据下标计算X坐标 // 计算Y坐标 float highY mapPriceToY(bean.high); float lowY mapPriceToY(bean.low); float openY mapPriceToY(bean.open); float closeY mapPriceToY(bean.close); Paint paint bean.close bean.open ? mUpPaint : mDownPaint; // 画影线 canvas.drawLine(x, highY, x, lowY, paint); // 画实体开盘收盘价格差距太小就画一条1px的线避免看不到实体 float top Math.min(openY, closeY); float bottom Math.max(openY, closeY); if (Math.abs(bottom - top) 1f) { bottom top 1f; } canvas.drawRect(x - candleWidth / 2f, top, x candleWidth / 2f, bottom, paint); } }这段逻辑里有两个容易忽略的点。一是开盘价等于收盘价时实体高度为0需要特殊处理不然十字星K线在屏幕上会看不到实际做法是画一条水平粗线。二是画笔要设置成描边模式同时填充实体如果只设置填充模式影线会变成两条细线视觉上很不自然。3.2 价格映射Y轴坐标的换算mapPriceToY是K线图最关键的坐标换算方法。绘制区的高度固定价格区间是动态的需要根据当前可见区间的最高价和最低价计算缩放比例private float mapPriceToY(float price) { if (mMaxPrice mMinPrice) return mChartTop; float ratio (price - mMinPrice) / (mMaxPrice - mMinPrice); return mChartTop (1f - ratio) * mChartHeight; }这里的mMaxPrice和mMinPrice不是全量数据的最大最小值而是当前可见区间内的最大最小值。因为如果以全部数据的最大最小值来算当用户放大到最近几十根K线时K线上下的波动会被压缩得很小看起来就像一条直线没法看。所以每次滑动或缩放结束都要重新计算可见区间的价格边界。3.3 十字光标和长按高亮十字光标是K线交互里非常影响体验的部分长按屏幕显示当前位置的十字线并且显示对应K线的开盘价、收盘价、最高价、最低价、涨跌幅等信息。实现思路是在onTouchEvent里监听ACTION_DOWN和ACTION_MOVE记录手指位置用X坐标反算出当前是第几根K线int index (int) ((touchX - mChartLeft) / (mCandleWidth mCandleSpacing)); index Math.max(mStartIndex, Math.min(mEndIndex, index)); // 防止越界然后调用invalidate()触发重绘。在onDraw里如果当前有十字光标状态就画十字线、绘制指示框、显示数据详情。十字光标的颜色、线宽、指示框的背景色按App主题风格来定。手感和性能上有个小技巧十字光标在移动时如果每次都导致整个View重绘K线多的时候还是会有点开销。可以把十字光标层用单独的View或者单独的Canvas图层来实现这样移动光标时只需要重绘画十字线和数据面板K线主体不用重绘体验会顺滑很多。3.4 均线、布林带和成交量副图主图的均线用Path来画把对应均线值依次连起来注意不要逐点drawLine而是moveTo和lineTo到一个Path里最后统一drawPath性能和效果都好得多。均线Path只需要在startIndex到endIndex范围内构建不需要全量构建。布林带的绘制类似只是多了一步计算上中下轨的数值然后用两条Path配合一个Paint的Style.FILL画一个半透明区间。成交量副图一般放在主图下方单独划分一个高度比例的区域。副图也需要独立的Y轴映射成交量最大值作为区间上限。注意主图和副图之间要留间隙方便画时间轴和标签。4. 交互体验与性能优化K线图能不能用一半看绘制一半看交互。这一节聚焦三个点触摸事件的处理、缩放滑动的实现、性能异常的排查。4.1 触摸事件点击、长按和滑动的判别K线图的触摸交互比较复杂正常滑动和长按光标是两个不同的操作要做手势判别。我实践中用的方案是在ACTION_DOWN时记录起点坐标和时间ACTION_MOVE时判断移动距离是否超过阈值一般是TouchSlop大概8dp超过就判定为滑动模式否则继续当作长按候选。用Handler发一个延迟任务500ms后如果还没有滑动就触发长按模式。一旦进入滑动模式要取消长按。滑动本身是通过改变mStartIndex和mEndIndex来实现的每根K线的宽度固定时手指滑动一个像素相当于移动若干分之一根K线。为了手感自然一般是让K线跟随手指实时移动而不是松手后再跳这对绘制性能要求比较高所以才会引出后面的绘制优化。4.2 双指缩放的最小和最大限制双指缩放是K线图刚需。实现上要用ScaleGestureDetector在onScale回调里修改candleWidth同时调整startIndex和endIndex让缩放中心点位于双指中心位置对应的K线上。缩放范围要有边界限制。我项目里设置最小K线宽度为3dp最大为30dp这个值可以根据屏宽和单屏显示数量来调整。比如一屏最少显示20根对应最大宽度最多显示120根对应最小宽度防止用户缩到只能看一根K线或者缩成一堆密集线条导致看不清。private void updateScale(float scaleFactor) { mCandleWidth * scaleFactor; if (mCandleWidth MIN_CANDLE_WIDTH) mCandleWidth MIN_CANDLE_WIDTH; if (mCandleWidth MAX_CANDLE_WIDTH) mCandleWidth MAX_CANDLE_WIDTH; // 重新计算可见区间保持缩放中心不变 mEndIndex mStartIndex (int) (getWidth() / (mCandleWidth mCandleSpacing)); if (mEndIndex mData.size()) mEndIndex mData.size() - 1; mStartIndex mEndIndex - (int) (getWidth() / (mCandleWidth mCandleSpacing)); mStartIndex Math.max(0, Math.min(mStartIndex, mData.size() - 1)); calculateVisibleMetrics(); // 重新计算可见区间价格边界 invalidate(); }这段代码的关键在于缩放前后的中心K线要尽量保持不变不然用户会觉得K线在漂移体验很差。具体做法是记录双指中心对应的K线下标缩放后用这个下标的X坐标位置反推新的startIndex。4.3 绘制优化避免卡顿的几条实战经验K线图的数据量上去了很容易掉帧尤其是老的Android机型。这一块踩过的坑比较多整理几条核心经验一是避免在onDraw里做复杂计算。所有能提前算的均线、指标、坐标映射都在数据更新时算好onDraw只做Canvas绘制动作。注意不要在onDraw里new对象像Paint、Path这些要作为成员变量复用。二是合理使用invalidate和postInvalidate。UI线程更新用invalidate子线程更新用postInvalidate。不要动不动就全图重绘局部区域用invalidate(Rect)效果不佳时就改为上面说的分层绘制把十字光标单独一层处理。三是数据分页加载。几千甚至上万条K线一次性全部加载进内存滑动回退时还要从List里取数据会造成启动慢和内存抖动。项目里我做了分页加载机制先加载最近200根滑到左侧边界时再向历史方向追加200根。这样初始加载速度大幅提升。四是关闭硬件加速的注意事项。K线图涉及的绘制场景硬件加速一般建议开启但如果用到了某些不支持加速的绘制API比如一些混合模式的PorterDuff会导致绘制结果异常。遇到绘制异常时先试着关闭硬件加速排查一下确认是API兼容性问题还是数据问题。实际项目中我遇到过一个机型在绘制大量Path时出现花屏关闭硬件加速后正常但整体性能下降约15%最终通过拆分Path解决没有牺牲加速。4.4 图表以外的协作联动滚动和双指缩放不少行情App里K线图和下面的成交量图是同一个维度的时间轴。在分层的方案里需要让主图和副图联动主图滚动或缩放时副图的startIndex和endIndex跟着变。这个联动不能通过分别计算实现容易产生不一致要抽出一个统一的可见区间管理类主图和副图共享同一个区间状态一方的变化通知另一方重绘。这也是为什么我之前强调可见区间是整个K线图实现的基石。5. 常见问题与排查技巧实录最后这块把实际项目中碰到过的典型问题列一下方便大家遇到时快速对号入座。5.1 常见问题速查表问题现象可能原因解决方案K线不显示或者显示空白数据为空、坐标映射计算出错maxPrice和minPrice相等检查数据源minPrice等于maxPrice时做特殊处理显示一条直线滑动时K线闪烁onDraw里创建了新的Paint或Path对象将Paint、Path提取为成员变量复用避免每次绘制创建对象十字光标显示位置不对点击X坐标转index的计算公式不考虑图表左边距计算公式要减去chartLeft的偏移量同时加上mCandleWidth/2修正双指缩放后K线漂移缩放时没有保持缩放中心不变记录缩放中心对应的K线下标缩放后反推startIndex大量数据加载卡顿一次性加载全量数据改为分页加载默认加载最近200根滑到边界再追加行情更新时K线闪动整个View直接invalidate重绘用局部更新或者只更新最后一根K线的区域5.2 独家排查技巧不管是用日志还是断点调试K线图最容易忽略的点是数据源本身有没有脏数据。有的行情数据接口会返回乱序的数据或者某根K线的high低于low。这种脏数据画出来光看几秒钟不一定发现问题但一缩放或者一算指标就各种异常。建议接到数据后先做一轮数据清洗把high低于low、open高于high这类异常值直接修正或者过滤掉。另外自定义View层级多了以后事件分发容易出问题。比如你把十字光标做成了覆盖在最上层的View这个View必须设置View.OnTouchListener并且返回false否则它会把触摸事件消费掉下层K线根本收不到手势。或者更稳妥的做法是光标层不设点击监听用setWillNotDraw(false)但把点击区域设成空让事件穿透到下层。还有一点关于真机调试。K线图的交互适配问题在模拟器上很容易被忽略。模拟器的触摸精度、双指缩放手感和真机有差异尤其是十字光标的移动平滑度必须在真机上验证。我在开发阶段都是用一台中低端Android手机作为基准机测试性能保证基准机流畅了高端机自然没问题。做K线图这个项目前后折腾了将近一个月回头看的几个转折点都在于对“可见区间”这个概念的理解和运用数据分页、指标计算、坐标映射、滑动缩放全部围绕它展开。另外就是要耐得住性子调细节蜡烛图的1像素实体、十字光标的跟手度、缩放中心的稳定性每一个细节打磨到位整个图表的体验才会有质的提升。如果有人后面要做类似的功能我的建议是先花两天时间彻底想清楚数据结构再用一天搭出能滑动缩放的骨架最后再慢慢打磨样式和交互不要一上来就陷入细节会走不少弯路。本文还有配套的精品资源点击获取
返回列表