1. 程式人生 > >Android版股票K線圖實現方案

Android版股票K線圖實現方案

前言

介紹

  K線圖一般分為日K、周K、月K,顯示的內容有開/收盤價、最高/低價、成交量,額外資訊為均線(ma5/10/20)。例如,日K圖中就為當日開/收盤價、最高/低價、成交量和5/10/20日均線。K線圖支援滑動,滑動過程中,動態改變最高最低價(和成交量);放大縮小可以由兩根手指觸發,雙擊也可以放大K線圖;節點應該由右向左按時間倒序分佈,支援“載入更多”。
  無論是日K還是周K、月K顯示的內容的模型其實是一樣,只是取樣週期不同,所以節點可以封裝為同一個,名為Entry,包含屬性:open、close、high、low、volume、ma5、ma10、ma20。顯然K線圖會同時顯示多個節點,那就封裝為EntryData,除了封裝有一個節點集合外,還封裝了對集合的所有相關操作,比如計算最大最小价、新增節點等。為了方便使用,可以將K線圖做成一個自定義控制元件,並且可以將所有繪製工作封裝在一起,名為Renderer,這個控制元件名當然就叫KLineChart了,由於滑動相關的實現與Android觸控事件處理息息相關,所以就寫在KLineChart類中了。

詳細設計

  我不打算將每行程式碼都在這篇文章中一一細說,本文就實現中的2個重點作講解,具體程式碼大家可以去檢視原始碼。
  首先要說的第一點就是繪製Entry。對於Entry的繪製,難點在於座標對映,也就是說要將Entry集合一一計算出最終在canvas上繪製的座標。這種對映邏輯可以用3個Matrix表示,下面我一一說明這3個Matrix的作用。
  value matrix:座標對映的第一步是將所有節點均勻分佈在整個canvas繪製區域內。對於x軸,我們要計算的只是拉伸量,可以用width/entrise.size表示,entrise表示的就是Entry集合;對於y軸,除了拉伸量外還有個平移量,這是因為所有節點的Y值並不是從0開始的,可以用height/yValueRange表示,yValueRange就是所有節點的Y值區間(最大值-最小值);最後由於要實現從右到左分佈節點,而且要y值越小的y座標越大,可以將拉伸量設定為負數,並平移width(height)距離:

mMatrixValue.postTranslate(0, -yMin);
mMatrixValue.postScale(-scaleX, -scaleY);
mMatrixValue.postTranslate(rect.width(),rect.height());

這樣value matrix就設定完成了,下面給張圖幫助理解:
enter image description here

  touch matrix:通過上面的value matrix變換後,我們就可以把當前集合中的所有節點對映到canvas上了,下一步拉伸(並平移)上面映射出的圖形,因為往往一屏並不能顯示出所有節點,為了支援滑動或者說當集合元素個數大於我們設定可繪製節點數時,就要對上一步變換後的圖形作拉伸,x軸拉伸量可以用entries.size/drawCount表示,drawCount就是我們設定的可繪製節點數量:

 mMatrixTouch.postScale(scaleX, scaleY);
 minTouchOffset = 0;
 maxTouchOffset = candleRect.width() * (scaleX - 1f);
 mMatrixTouch.postTranslate(-maxTouchOffset, 0);

其中y軸不需要拉伸,所以scaleY取1。有了這個x軸的拉伸量後,也就可以計算出x軸的最大最小平移量了,如上程式碼所示。下面給張圖幫助理解:
enter image description here

  offset matrix:這個matrix相對來說較為簡單,它的作用是上面變換後的圖形作平移,空出繪製y軸和x軸的區間。

mMatrixOffset.postTranslate(offsetX, offsetY);

這個平移大小y軸並沒有特殊限制了,而x軸的平移大小應該和y值的繪製區間相關。下面給張圖幫助理解:
enter image description here

  通過上面3個矩陣變換後就可以得到最終的繪製點了,其實這本可以用1個矩陣來表示的,之所以分成3個最主要的原因是滑動過程中,我們只需要更新touch matrix的x軸的平移量便可,而平移的變化量自然就是由手指滑動距離來計算的了。
  下面就開始介紹實現中的另一個難點了——滑動。與RecyclerView相同,定義滑動分為2部分scroll與fling,其中scroll是由觸屏事件ACTION_MOVE觸發的,fling是由觸屏事件ACTION_UP觸發的。
  我們先通過ViewConfiguration獲得啟動滑動的最小位移量,名為touch slop,這個常量的用法是:當滑動偏移量首次大於這個值時,之後的ACTION_MOVE事件,才能識別為使用者滑屏事件。當將ACTION_MOVE視為滑屏事件時,計算滑動偏移量dx,然後將這個偏移量作為x軸的平移增量更新touch matrix:

mMatrixTouch.getValues(matrixValues);

matrixValues[Matrix.MTRANS_X] += -dx;
matrixValues[Matrix.MTRANS_Y] += dy;

if (matrixValues[Matrix.MTRANS_X] < -maxTouchOffset) {
  matrixValues[Matrix.MTRANS_X] = -maxTouchOffset;
}
if (matrixValues[Matrix.MTRANS_X] > 0) {
  matrixValues[Matrix.MTRANS_X] = 0;
}

mMatrixTouch.setValues(matrixValues);

這樣再通過3個matrix變換後得到的Entry座標就是我們滑動過後的新座標,最後重繪UI。
  fling的滑動是藉助Scroller類實現的。捕獲ACTION_UP事件,通過VelocityTracker類計算得到事件觸發時的滑動初速度,在K線圖中只用處理x軸的即可,這個初速度我命名為velocityX,對velocityX作邊界檢測,這個很好理解,速度不能太快或太慢,邊界值或叫閥值同樣可以通過ViewConfiguration獲得;因為fling是一個自發的連續性動畫,方法View.postOnAnimation(Runnable)可以使給定的Runnable物件在下一幀繪製時執行,我們可以利用這個機制,來實現一個“類遞迴”功能,以驅動fling的“自發及連續性”,將這個功能封裝在類ViewFlinger中:

class ViewFlinger implements Runnable{
  public void run{
    final int x = scroller.getCurrX();
    final int y = scroller.getCurrY();
    final int dx = x - mLastFlingX;
    final int dy = y - mLastFlingY;
    mLastFlingX = x;
    mLastFlingY = y;
    scroll(dx, 0);

    if (!scroller.isFinished()) {
    postOnAnimation();
    }
  }
  public void fling(velocityX,velocityY){
    scroller.fling(0,0,velocityX,velocityY,...);
    postOnAnimation();
  }
}

上面就是ViewFling中的關鍵程式碼了,可以看出方法fling()就是觸發這個“類遞迴”執行的地方,然後在這個Runnable執行過程中,呼叫的scroll()方法其實就是上文所講的scroll過程中更新touch matrix並重繪UI的方法,只是給定的x軸平移增量是由Scroller計算得到的。至此K線圖實現的2個難點就講完了,如有不對,歡迎指正。最後再給張相對完整的演示圖:
enter image description here

結束語

  在實現基本的K線圖功能中,對於座標對映與滑動這2個難點的實現方案,其實並不是我想出來的。其中,座標對映的方案來自開源專案MPAndroidChart,而滑動的方案取自RecyclerView。這就叫學以致用:)