/ 今日科技快讯 /
近日,蔚来披露合肥战略投资协议重大进展,已按计划实质性完成注资。今年4月29日,蔚来官方宣布,蔚来中国总部落户合肥项目协议正式签署,战略投资者将向蔚来中国投资70亿元人民币。蔚来今年已累计完成融资超百亿人民币融资。
/ 作者简介 /
本篇文章来自彼岸人生的投稿,分享了如何从无到有绘制股票行情图,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!
彼岸人生的博客地址:
https://me.csdn.net/kemeng7758
/ 前言 /
股票??数字货币??都是浮云,没那智商还是好好撸代码吧!今天作为一个嫩绿嫩绿的韭菜,就来用技术征服一下割过自己的股票行情图。
股票行情图中比较复杂的应该当属于蜡烛线(阴阳线),这块手势处理复杂、图表指标复杂、交互复杂、数据处理复杂......总之:复杂!
所以就从今天开始我从0到1打造出这个复杂的行情图!费话不多说,上图!上链接:
github地址:
https://github.com/SlamDunk007/StockChart
/ 绘制流程 /
整个绘制过程完全自定义View不依赖任何第三方绘制工具,大概分为三个部分:具体的绘制过程、手势的处理、数据的处理。下面就从这三个方面逐个进行讲解。
具体绘制过程
这里使用的是Android的canvas进行绘制的,android的canvas真的是特别的强大,为了调高绘制效率,我在这里的绘制进行了修改:提前创建一个Canvas和Bitmap,然后在子线程当中进行绘制:
private void initCanvas() { repeatNum = 0; if (mRealCanvas == null) { mRealCanvas = new Canvas(); Bitmap curBitmap = createBitmap(mViewPortHandler.getChartWidth(), mViewPortHandler.getChartHeight(), Bitmap.Config.ARGB_8888); Bitmap alterBitmap = curBitmap.copy(Bitmap.Config.ARGB_8888, true); if (curBitmap != null && alterBitmap != null) { mRealCanvas.setBitmap(curBitmap); mCurBitmap = curBitmap; mAlterBitmap = alterBitmap; } } }接下来采用双缓冲的绘图机制,先在子线程当中将所有的图像都绘制到一个Bitmap对象上,然后一次性将内存中的Bitmap绘制到屏幕,提高绘制的效率。Android中View的onDraw()方法已经实现了这一层缓冲。onDraw()方法中不是绘制一点显示一点,而是全部绘制完之后一次性显示到屏幕。
/** * 进行具体的绘制 */ class DoubleBuffering implements Runnable { private final WeakReference<BaseChartView> mChartView; public DoubleBuffering(BaseChartView view) { mChartView = new WeakReference<>(view); } @Override public synchronized void run() { if (mChartView != null) { BaseChartView baseChartView = mChartView.get(); if (baseChartView != null && baseChartView.mRealCanvas != null) { baseChartView.drawFrame(baseChartView.mRealCanvas); Bitmap bitmap = baseChartView.mCurBitmap; if (bitmap != null && baseChartView.mHandler != null) { baseChartView.mHandler.sendEmptyMessage(baseChartView.REFRESH); } } } } }然后将我们绘制完成的bitmap对象交给View的onDraw()方法的canvas去绘制
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mRealBitmap != null) { canvas.drawBitmap(mRealBitmap, 0, 0, mPaint); } if (hasDrawed) { hasDrawed = false; if (!mHandler.hasMessages(START_PAINT)) { Message message = new Message(); message.what = START_PAINT; message.obj = mDoubleBuffering; mHandler.sendMessageDelayed(message, 25); } } }这是整个绘制流程的关键代码,和平时的自定义绘制没有什么特殊的区别,只不过这里采用了双缓冲的绘图机制。提前绘制到一个Bitmap上去。
我做过一个简单的测试,当绘制的视图比较复杂的时候,如果提前进行绘制,打开开发者的呈现模式,可以发现越复杂的视图,对GPU的消耗减少的越明显,这里大家可以写一个demo简单测试一下,这里不再赘述。
蜡烛线、长按十字线和长按弹框的具体绘制
长按手势的识别方法可以继续参考下面的手势的处理部分。
蜡烛线:股票的蜡烛线有高、开、低、收四个参数,分别代表:最高价、开盘价、最低价、收盘价。这里首先计算出最高价当中的最大值和最低价当中的最小值,然后根据(maxPrice<最高价> - openPrice<开盘价>)/diffPrice<最高价-最低价>,计算出蜡烛线的上影线,下影线,开盘价,收盘价的占比。从而就能计算出在绘制区域的具体位置。
// 计算蜡烛线 float scaleY_open = (maxPrice - open) / diffPrice; float scaleY_low = (maxPrice - close) / diffPrice; RectF candleRect = getRect(contentRect, k, scaleY_open, scaleY_low); drawItem.rect = candleRect; // 计算上影线,下影线 float scale_HL_T = (maxPrice - high) / diffPrice; float scale_HL_B = (maxPrice - low) / diffPrice; RectF shadowRect = getLine(contentRect, k, scale_HL_T, scale_HL_B); drawItem.shadowRect = shadowRect;长按十字线和弹框:这个是根据长按的动作然后在右上角的位置,获取最后一天的高开低收等数据,最后重新绘制当前屏幕。
// 绘制长按十字线 if (mFocusPoint != null && onLongPress) { if (contentRect.contains(mFocusPoint.x, mFocusPoint.y)) { canvas.drawLine(contentRect.left, mFocusPoint.y, contentRect.right, mFocusPoint.y, PaintUtils.FOCUS_LINE_PAINT); } canvas.drawLine(mFocusPoint.x, contentRect.top, mFocusPoint.x, contentRect.bottom, PaintUtils.FOCUS_LINE_PAINT); KLineToDrawItem item = mToDrawList.get(mFocusIndex); drawBollDes(canvas, contentRect, item); } // 长按显示的弹框 showLongPressDialog(canvas, contentRect);手势的处理
代码当中的ChartTouchHelper是处理手势的关键类,目前行情图的手势有几种:左右滑动DRAG、惯性滑动FLING、放大缩小Scale、长按LONG_PRESS。
这里使用了android当中的GestureDetectorCompat结合onTouch(View v, MotionEvent event)来处理这几种手势。
左右滑动DRAG
实现OnGestureListener接口,有一个onScroll的方法,在这里将X轴移动的距离当做偏移量,一屏默认显示的蜡烛线是60个,根据偏移量可以计算出移动了多少个蜡烛线,然后就能根据这个去计算下一次绘制的起始点的位置,重新计算滑动后的屏幕的数据。最后Invalidate一下,重新进行绘制即可。
/** * @param e1 down的时候event * @param e2 move的时候event * @param distanceX x轴移动距离:两个move之间差值 * @param distanceY y轴移动距离 */ @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (mChartGestureListener != null) { scrollX -= distanceX; // 当X轴移动距离大于18px认为是移动 if (Math.abs(scrollX) > mXMoveDist) { mChartGestureListener.onChartTranslate(e2, scrollX); scrollX = 0; } } if (Math.abs(distanceX) > Math.abs(distanceY)) { return true; } else { return false; } }惯性滑动FLING
当手指快速滑动离开的那一瞬间,有一个初始速度。通过SensorManager计算出加速度,根据公式a=V^2/2S(加速度等于最大速度的平方除以2倍的路程),可以反推出S=V^2/2a,计算出加速度减为0的时候,总共Fling的距离。这里默认是匀减速运动,然后使用手指离开时的速度/加速度=总共耗时duration,最后就可以根据上面这些数据计算出每时间内移动的距离,把这个距离当做偏移量去计算我们的数据起始位置,重新绘制即可。
/** * @param e1 手指按下的位置 * @param e2 手指抬起的位置 * @param velocityX 手指抬起时的x轴的加速度 px/s */ @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mLastGesture = ChartGesture.FLING; fling(velocityX, e2.getX() - e1.getX()); return true; } private void fling(float velocity, float offset) { stopFling(); if (Math.abs(mDeceleration) > DataUtils.EPSILON) { // 根据加速度计算速度减少到0时的时间 int duration = (int) (1000 * velocity / mDeceleration); // 手指抬起时,缓冲的距离 int totalDistance = (int) ((velocity * velocity) / (mDeceleration + mDeceleration)); int startX = (int) offset, flingX; if (velocity < 0) { flingX = startX - totalDistance; } else { flingX = startX + totalDistance; } mFlingRunnable = new FlingRunnable(startX, flingX, duration, mHandler, mChartGestureListener); mHandler.post(mFlingRunnable); } }放大缩小SCALE
放大缩小的处理稍微就简单了一些,这里监听MotionEvent.ACTION_POINTER_DOWN这个手势,这个手势处理的就是多指按下的情况,根据多指的按下位置和缩放之后的位置计算出一个缩放比出来。然后动态的去更改一屏默认显示的蜡烛线个数,并且更改绘制的起始位置,刷新即可。
case MotionEvent.ACTION_POINTER_DOWN: if (event.getPointerCount() >= 2) { saveTouchStart(event); // 两个手指之间在X轴的距离 mSavedXDist = getXDist(event); // 两个手指之间的距离 mSavedDist = spacing(event); // 两个手指之间距离大于10才认为是缩放 if (mSavedDist > 10f) { mTouchMode = X_ZOOM; } // 计算两个手指之间的中点位置 midPoint(mTouchPointCenter, event); } break;根据移动后的位置计算缩放比
case MotionEvent.ACTION_MOVE: if (mTouchMode == DRAG) { mLastGesture = ChartGesture.DRAG; } else if (mTouchMode == X_ZOOM) { if (event.getPointerCount() >= 2) { // 手指移动的距离 float totalDist = spacing(event); if (totalDist > mMinScalePointerDistance) { if (mTouchMode == X_ZOOM) { mLastGesture = ChartGesture.X_ZOOM; float xDist = getXDist(event); float scaleX = xDist / mSavedXDist; if (mChartGestureListener != null) { mChartGestureListener.onChartScale(event, scaleX, 1); } } } } }长按LONG_PRESS
长按的处理是简单的,直接实现接口中的onLongPress方法即可知道当前长按的位置。然后根据长按动作去处理十字线以及长按的弹框等
@Override public void onLongPress(MotionEvent e) { mTouchMode = LONG_PRESS; if (mChartGestureListener != null) { mChartGestureListener.onChartLongPressed(e); } }使用ChartDataSourceHelper和TechParamsHelper(相关技术指标的计算),根据上面手势移动的偏移量、缩放比进行数据的重组,这块可以直接参考源码阅读即可,没有什么特别复杂的地方。
根据初始位置计算初始化数据
/** * 初始化行情图初始数据 */ public void initKDrawData(List<KLineItem> klineList, KMasterChartView kLineChartView, KSubChartView volumeView, KSubChartView macdView) { this.mKList = klineList; this.mKLineChartView = kLineChartView; this.mVolumeView = volumeView; this.mMacdView = macdView; mSubChartData = new SubChartData(); // K线首次当前屏初始位置 startIndex = Math.max(0, klineList.size() - K_D_COLUMNS); // k线首次当前屏结束位置 endIndex = klineList.size() - 1; // 计算技术指标 mTechParamsHelper.caculateTechParams(klineList, TechParamType.BOLL); mTechParamsHelper.caculateTechParams(klineList, TechParamType.MACD); initKMoveDrawData(0, SourceType.INIT); }当横向滑动、Fling惯性滑动和缩放之后,重新计算初始位置和当前屏幕的蜡烛线等
/** * 根据移动偏移量计算行情图当前屏数据 * * @param distance 手指横向移动距离 */ public void initKMoveDrawData(float distance, SourceType sourceType) { // 重置默认值 resetDefaultValue(); // 计算当前屏幕开始和结束的位置 countStartEndPos(distance, sourceType); // 计算蜡烛线价格最大最小值,成交量最大值 ExtremeValue extremeValue = countMaxMinValue(); // 最大值最小值差值 float diffPrice = maxPrice - minPrice; // MACD最大最小值 float diffMacd = maxMacd - minMacd; float diffBoll = maxBoll - minBoll; RectF contentRect = mKLineChartView.getViewPortHandler().mContentRect; // 计算当前屏幕每一个蜡烛线的位置和涨跌情况 for (int i = startIndex, k = 0; i < endIndex; i++, k++) { KLineItem kLineItem = mKList.get(i); // 开盘价 float open = kLineItem.open; // 最低价 float close = kLineItem.close; // 最高价 float high = kLineItem.high; // 最低价 float low = kLineItem.low; KLineToDrawItem drawItem = new KLineToDrawItem(); // 计算蜡烛线 float scaleY_open = (maxPrice - open) / diffPrice; float scaleY_low = (maxPrice - close) / diffPrice; RectF candleRect = getRect(contentRect, k, scaleY_open, scaleY_low); drawItem.rect = candleRect; // 计算上影线,下影线 float scale_HL_T = (maxPrice - high) / diffPrice; float scale_HL_B = (maxPrice - low) / diffPrice; RectF shadowRect = getLine(contentRect, k, scale_HL_T, scale_HL_B); drawItem.shadowRect = shadowRect; // 计算红涨绿跌,暂时这么计算(其实红涨绿跌是根据当前开盘价和前一天的收盘价做对比) if (i - 1 >= 0) { KLineItem preItem = mKList.get(i - 1); if (kLineItem.open > preItem.close) { drawItem.isFall = false; } else { drawItem.isFall = true; } if (preItem.close != 0) { kLineItem.preClose = preItem.close; } else { kLineItem.preClose = kLineItem.open; } } // 计算每一个月的第一个交易日 if (i - 1 >= 0 && i + 1 < endIndex) { int currentMonth = DateUtils.getMonth(kLineItem.day); KLineItem preItem = mKList.get(i - 1); int preMonth = DateUtils.getMonth(preItem.day); if (currentMonth != preMonth) { drawItem.date = kLineItem.day.substring(0, 10); } } // 计算成交量 if (Math.abs(maxVolume) > DataUtils.EPSILON) { RectF volumeRct = mVolumeView.getViewPortHandler().mContentRect; float scaleVolume = (maxVolume - kLineItem.volume) / maxVolume; drawItem.volumeRect = getRect(volumeRct, k, scaleVolume, 1); } // 计算BOLL caculateBollPath(diffBoll, contentRect, i, k, drawItem); // 计算附图MACD Path caculateMacdPath(diffMacd, i, k, drawItem.isFall); drawItem.klineItem = kLineItem; kLineItems.add(drawItem); } List<KLineToDrawItem> resultList = new ArrayList<>(); // 数据准备完毕 if (mReadyListener != null) { resultList.addAll(kLineItems); mReadyListener.onReady(resultList, extremeValue, mSubChartData); } }/ 总结 /
目前市面上有很多的自定义图表,但是能将行情图以及各项指标完全复用的基本上没有,比较牛逼的就是MPChart基本上能够满足大部分的图表使用,但是对行情图来说还是远远不够。所以出于兴趣,就模仿火币和炒股软件进行了一个自定义蜡烛线,由于不是专业人士,可能有的金融指标有一些偏差,这里明白绘制技术即可,不必关心这些金融细节。
规划(项目会继续完善更新):
后面会继续丰富图标的各项指标
数据层要进行整理,目前有些地方处理不是特别高效
实现各种图表动态添加、切换等。
github地址:
https://github.com/SlamDunk007/StockChart
推荐阅读:
JNI NDK入门详解,包含常见问题解答!
Jetpack家族新成员,App Startup学习笔记
快速迭代,PermissionX现在支持Java了!
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注