花花綠綠的股票線是怎么畫出來的?想怎么畫就怎么畫!
? ? ?
? ?正文? ?
股票??數(shù)字貨幣??都是浮云,沒那智商還是好好擼代碼吧!今天作為一個嫩綠嫩綠的韭菜,就來用技術(shù)征服一下割過自己的股票行情圖。
股票行情圖中比較復雜的應該當屬于蠟燭線(陰陽線),這塊手勢處理復雜、圖表指標復雜、交互復雜、數(shù)據(jù)處理復雜......總之:復雜!
所以就從今天開始我從0到1打造出這個復雜的行情圖!費話不多說,上圖!上鏈接:


github地址:
github.com/SlamDunk007/StockChart
/? ?繪制流程? ?/
整個繪制過程完全自定義View不依賴任何第三方繪制工具,大概分為三個部分:具體的繪制過程、手勢的處理、數(shù)據(jù)的處理。下面就從這三個方面逐個進行講解。
具體繪制過程
這里使用的是Android的canvas進行繪制的,android的canvas真的是特別的強大,為了調(diào)高繪制效率,我在這里的繪制進行了修改:提前創(chuàng)建一個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對象上,然后一次性將內(nèi)存中的Bitmap繪制到屏幕,提高繪制的效率。Android中View的onDraw()方法已經(jīng)實現(xiàn)了這一層緩沖。onDraw()方法中不是繪制一點顯示一點,而是全部繪制完之后一次性顯示到屏幕。
/**
???*?進行具體的繪制
???*/
??class?DoubleBuffering?implements?Runnable?{
????private?final?WeakReference?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);
??????}
????}
??}
這是整個繪制流程的關(guān)鍵代碼,和平時的自定義繪制沒有什么特殊的區(qū)別,只不過這里采用了雙緩沖的繪圖機制。提前繪制到一個Bitmap上去。
我做過一個簡單的測試,當繪制的視圖比較復雜的時候,如果提前進行繪制,打開開發(fā)者的呈現(xiàn)模式,可以發(fā)現(xiàn)越復雜的視圖,對GPU的消耗減少的越明顯,這里大家可以寫一個demo簡單測試一下,這里不再贅述。
蠟燭線、長按十字線和長按彈框的具體繪制
長按手勢的識別方法可以繼續(xù)參考下面的手勢的處理部分。
蠟燭線:股票的蠟燭線有高、開、低、收四個參數(shù),分別代表:最高價、開盤價、最低價、收盤價。這里首先計算出最高價當中的最大值和最低價當中的最小值,然后根據(jù)(maxPrice<最高價> - openPrice<開盤價>)/diffPrice<最高價-最低價>,計算出蠟燭線的上影線,下影線,開盤價,收盤價的占比。從而就能計算出在繪制區(qū)域的具體位置。
//?計算蠟燭線
?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;
長按十字線和彈框:這個是根據(jù)長按的動作然后在右上角的位置,獲取最后一天的高開低收等數(shù)據(jù),最后重新繪制當前屏幕。
//?繪制長按十字線
????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是處理手勢的關(guān)鍵類,目前行情圖的手勢有幾種:左右滑動DRAG、慣性滑動FLING、放大縮小Scale、長按LONG_PRESS。
這里使用了android當中的GestureDetectorCompat結(jié)合onTouch(View v, MotionEvent event)來處理這幾種手勢。
左右滑動DRAG
實現(xiàn)OnGestureListener接口,有一個onScroll的方法,在這里將X軸移動的距離當做偏移量,一屏默認顯示的蠟燭線是60個,根據(jù)偏移量可以計算出移動了多少個蠟燭線,然后就能根據(jù)這個去計算下一次繪制的起始點的位置,重新計算滑動后的屏幕的數(shù)據(jù)。最后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計算出加速度,根據(jù)公式a=V^2/2S(加速度等于最大速度的平方除以2倍的路程),可以反推出S=V^2/2a,計算出加速度減為0的時候,總共Fling的距離。這里默認是勻減速運動,然后使用手指離開時的速度/加速度=總共耗時duration,最后就可以根據(jù)上面這些數(shù)據(jù)計算出每時間內(nèi)移動的距離,把這個距離當做偏移量去計算我們的數(shù)據(jù)起始位置,重新繪制即可。
/**
???*?@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)?{
??????//?根據(jù)加速度計算速度減少到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
放大縮小的處理稍微就簡單了一些,這里監(jiān)聽MotionEvent.ACTION_POINTER_DOWN這個手勢,這個手勢處理的就是多指按下的情況,根據(jù)多指的按下位置和縮放之后的位置計算出一個縮放比出來。然后動態(tài)的去更改一屏默認顯示的蠟燭線個數(shù),并且更改繪制的起始位置,刷新即可。
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;
根據(jù)移動后的位置計算縮放比
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
長按的處理是簡單的,直接實現(xiàn)接口中的onLongPress方法即可知道當前長按的位置。然后根據(jù)長按動作去處理十字線以及長按的彈框等
@Override
??public?void?onLongPress(MotionEvent?e)?{
????mTouchMode?=?LONG_PRESS;
????if?(mChartGestureListener?!=?null)?{
??????mChartGestureListener.onChartLongPressed(e);
????}
??}
數(shù)據(jù)的處理
使用ChartDataSourceHelper和TechParamsHelper(相關(guān)技術(shù)指標的計算),根據(jù)上面手勢移動的偏移量、縮放比進行數(shù)據(jù)的重組,這塊可以直接參考源碼閱讀即可,沒有什么特別復雜的地方。
根據(jù)初始位置計算初始化數(shù)據(jù)
/**
???*?初始化行情圖初始數(shù)據(jù)
???*/
??public?void?initKDrawData(List?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線首次當前屏結(jié)束位置
????endIndex?=?klineList.size()?-?1;
????//?計算技術(shù)指標
????mTechParamsHelper.caculateTechParams(klineList,?TechParamType.BOLL);
????mTechParamsHelper.caculateTechParams(klineList,?TechParamType.MACD);
????initKMoveDrawData(0,?SourceType.INIT);
??}
當橫向滑動、Fling慣性滑動和縮放之后,重新計算初始位置和當前屏幕的蠟燭線等
/**
???*?根據(jù)移動偏移量計算行情圖當前屏數(shù)據(jù)
???*
???*?@param?distance?手指橫向移動距離
???*/
??public?void?initKMoveDrawData(float?distance,?SourceType?sourceType)?{
????//?重置默認值
????resetDefaultValue();
????//?計算當前屏幕開始和結(jié)束的位置
????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???????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;
??????//?計算紅漲綠跌,暫時這么計算(其實紅漲綠跌是根據(jù)當前開盤價和前一天的收盤價做對比)
??????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?????????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?resultList?=?new?ArrayList<>();
????//?數(shù)據(jù)準備完畢
????if?(mReadyListener?!=?null)?{
??????resultList.addAll(kLineItems);
??????mReadyListener.onReady(resultList,?extremeValue,?mSubChartData);
????}
??}
? ??/ ? 總結(jié)? ?/
目前市面上有很多的自定義圖表,但是能將行情圖以及各項指標完全復用的基本上沒有,比較牛逼的就是MPChart基本上能夠滿足大部分的圖表使用,但是對行情圖來說還是遠遠不夠。所以出于興趣,就模仿火幣和炒股軟件進行了一個自定義蠟燭線,由于不是專業(yè)人士,可能有的金融指標有一些偏差,這里明白繪制技術(shù)即可,不必關(guān)心這些金融細節(jié)。
規(guī)劃(項目會繼續(xù)完善更新):
后面會繼續(xù)豐富圖標的各項指標 數(shù)據(jù)層要進行整理,目前有些地方處理不是特別高效 實現(xiàn)各種圖表動態(tài)添加、切換等。
github.com/SlamDunk007/StockChart
來源:https://me.csdn.net/kemeng7758
版權(quán)申明:內(nèi)容來源網(wǎng)絡,版權(quán)歸原創(chuàng)者所有。除非無法確認,我們都會標明作者及出處,如有侵權(quán)煩請告知,我們會立即刪除并表示歉意。謝謝!

