一篇小文章讓你了解花花綠綠股票線的來源!

近日,蔚來披露合肥戰(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來獲取哦。


?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容