近日,蔚來披露合肥戰(zhàn)略投資協(xié)議重大進展,已按計劃實質(zhì)性完成注資。今年4月29日,蔚來官方宣布,蔚來中國總部落戶合肥項目協(xié)議正式簽署,戰(zhàn)略投資者將向蔚來中國投資70億元人民幣。蔚來今年已累計完成融資超百億人民幣融資。
/ 前言 /
股票??數(shù)字貨幣??都是浮云,沒那智商還是好好擼代碼吧!今天作為一個嫩綠嫩綠的韭菜,就來用技術征服一下割過自己的股票行情圖。
股票行情圖中比較復雜的應該當屬于蠟燭線(陰陽線),這塊手勢處理復雜、圖表指標復雜、交互復雜、數(shù)據(jù)處理復雜......總之:復雜!
所以就從今天開始我從0到1打造出這個復雜的行情圖!費話不多說,上圖!上鏈接:


/ 繪制流程 /
整個繪制過程完全自定義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<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);
}
}
}
是整個繪制流程的關鍵代碼,和平時的自定義繪制沒有什么特殊的區(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是處理手勢的關鍵類,目前行情圖的手勢有幾種:左右滑動DRAG、慣性滑動FLING、放大縮小Scale、長按LONG_PRESS。
這里使用了android當中的GestureDetectorCompat結合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=V2/2S(加速度等于最大速度的平方除以2倍的路程),可以反推出S=V2/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(相關技術指標的計算),根據(jù)上面手勢移動的偏移量、縮放比進行數(shù)據(jù)的重組,這塊可以直接參考源碼閱讀即可,沒有什么特別復雜的地方。
根據(jù)初始位置計算初始化數(shù)據(jù)
/**
* 初始化行情圖初始數(shù)據(jù)
*/
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慣性滑動和縮放之后,重新計算初始位置和當前屏幕的蠟燭線等
/**
* 根據(jù)移動偏移量計算行情圖當前屏數(shù)據(jù)
*
* @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;
// 計算紅漲綠跌,暫時這么計算(其實紅漲綠跌是根據(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 < 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<>();
// 數(shù)據(jù)準備完畢
if (mReadyListener != null) {
resultList.addAll(kLineItems);
mReadyListener.onReady(resultList, extremeValue, mSubChartData);
}
}
/ 總結 /
目前市面上有很多的自定義圖表,但是能將行情圖以及各項指標完全復用的基本上沒有,比較牛逼的就是MPChart基本上能夠滿足大部分的圖表使用,但是對行情圖來說還是遠遠不夠。所以出于興趣,就模仿火幣和炒股軟件進行了一個自定義蠟燭線,由于不是專業(yè)人士,可能有的金融指標有一些偏差,這里明白繪制技術即可,不必關心這些金融細節(jié)。
規(guī)劃(項目會繼續(xù)完善更新):
后面會繼續(xù)豐富圖標的各項指標
數(shù)據(jù)層要進行整理,目前有些地方處理不是特別高效
實現(xiàn)各種圖表動態(tài)添加、切換等。
以下是我所整理出來的一份資料,想要的小伙伴可以點擊我的GitHub來獲取哦。

