天天看點

android 深度圖

在 Android開發中自定義控件是一個範圍很廣的話題,講起自定義控件,從廣度上來劃分的話,大體上可以劃分為:

  • View、ViewGroup的繪制
  • 事件分發
  • 各種動畫效果
  • 滾動嵌套機制
  • 還有涉及到相關的數學知識等等

本次來講講如何實作交易所中的K線圖,首先通過一張深度圖開始講解下相關業務需求

android 深度圖

深度圖一般代表交易所目前買入和賣出的委托量(不是指成交),從這張圖我們可以看出X軸代表價格,價格從左往右依次增高,Y軸代表銷量,由下往上遞增,要繪制出正常的深度圖,每次擷取的資料至少包含價格和銷量,通常的時間,開盤,收盤,高,低價都可忽略。實際開發中這塊擷取資料使用長連結即可,背景每次傳回規定的買入和賣出的資料數量,要先處理背景的傳回資料:

  • 首先将傳回的買入和賣出的資料分開按照價格從低到高排序
  • 然後再去處理每個價格對應的委托量,因為傳回的目前價格對應的委托量是無法對應深度圖的坐标軸,我們需要将按價格排序後,高的價格去加上上一個價格的委托量,這樣才可保證委托量的展示是在遞增

處理完傳回的資料後重新填充資料即可。處理完資料後就要開始處理互動方面了,當使用者點選或者長按的時候就要展示目前選中點的相關資料了,從圖中可以看到選中的時候有個圓圈以及在X,Y軸上展示了此坐标的價格和委托量。

先上效果圖:

android 深度圖

由于上傳大小的限制,修改了gif的品質,是以效果不是很好。

通過上面的實作效果可以看到做了一點功能上的簡化,長按之後我并沒有将結果展示在X,Y軸上而是直接顯示在中間,不過這些都是次要的,最重要的是了解思路,看了源碼後可以根據自己的需求修改。

首先講下從背景擷取到資料的處理,先将資料按價格進行排序,然後通過周遊下集合,将每個bean對象的委托量的數值累加下上一個的然後重新指派,同時擷取買入和賣出的最高和最低價格,後面會用到資料展示

public void setData(List<DepthDataBean> buyData, List<DepthDataBean> sellData) {
        float vol = 0;
        if (buyData.size() > 0) {
            mBuyData.clear();
            //買入資料按價格進行排序
            Collections.sort(buyData, new comparePrice());
            DepthDataBean depthDataBean;
            //累加買入委托量
            for (int index = buyData.size() - 1; index >= 0; index--) {
                depthDataBean = buyData.get(index);
                vol += depthDataBean.getVolume();
                depthDataBean.setVolume(vol);
                mBuyData.add(0, depthDataBean);
            }
            //修改底部買入價格展示
            mBottomPrice[0] = mBuyData.get(0).getPrice();
            mBottomPrice[1] = mBuyData.get(mBuyData.size() > 1 ? mBuyData.size() - 1 : 0).getPrice();
            mMaxVolume = mBuyData.get(0).getVolume();
        }

        if (sellData.size() > 0) {
            mSellData.clear();
            vol = 0;
            //賣出資料按價格進行排序
            Collections.sort(sellData, new comparePrice());
            //累加賣出委托量
            for (DepthDataBean depthDataBean : sellData) {
                vol += depthDataBean.getVolume();
                depthDataBean.setVolume(vol);
                mSellData.add(depthDataBean);
            }
            //修改底部賣出價格展示
            mBottomPrice[2] = mSellData.get(0).getPrice();
            mBottomPrice[3] = mSellData.get(mSellData.size() > 1 ? mSellData.size() - 1 : 0).getPrice();
            mMaxVolume = Math.max(mMaxVolume, mSellData.get(mSellData.size() - 1).getVolume());
        }
        mMaxVolume = mMaxVolume * 1.05f;
        mMultiple = mMaxVolume / mLineCount;
        invalidate();
    }

           

資料處理好後就要開始繪制了

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(mBackgroundColor);
        canvas.save();
        //繪制買入區域
        drawBuy(canvas);
        //繪制賣出區域
        drawSell(canvas);
        //繪制界面相關文案
        drawText(canvas);
        canvas.restore();
    }
    
    private void drawBuy(Canvas canvas) {
        mGridWidth = (mDrawWidth * 1.0f / (mBuyData.size() - 1 == 0 ? 1 : mBuyData.size() - 1));
        mBuyPath.reset();
        mMapX.clear();
        mMapY.clear();
        float x;
        float y;
        for (int i = 0; i < mBuyData.size(); i++) {
            if (i == 0) {
                mBuyPath.moveTo(0, getY(mBuyData.get(0).getVolume()));
            }
            y = getY(mBuyData.get(i).getVolume());
            if (i >= 1) {
                canvas.drawLine(mGridWidth * (i - 1), getY(mBuyData.get(i - 1).getVolume()), mGridWidth * i, y, mBuyLinePaint);
            }
            if (i != mBuyData.size() - 1) {
                mBuyPath.quadTo(mGridWidth * i, y, mGridWidth * (i + 1), getY(mBuyData.get(i + 1).getVolume()));
            }

            x = mGridWidth * i;
            mMapX.put((int) x, mBuyData.get(i));
            mMapY.put((int) x, y);
            if (i == mBuyData.size() - 1) {
                mBuyPath.quadTo(mGridWidth * i, y, mGridWidth * i, mDrawHeight);
                mBuyPath.quadTo(mGridWidth * i, mDrawHeight, 0, mDrawHeight);
                mBuyPath.close();
            }
        }
        canvas.drawPath(mBuyPath, mBuyPathPaint);
	}

    private void drawSell(Canvas canvas) {
        mGridWidth = (mDrawWidth * 1.0f / (mSellData.size() - 1 == 0 ? 1 : mSellData.size() - 1));
        mSellPath.reset();
        float x;
        float y;
        for (int i = 0; i < mSellData.size(); i++) {
            if (i == 0) {
                mSellPath.moveTo(mDrawWidth, getY(mSellData.get(0).getVolume()));
            }
            y = getY(mSellData.get(i).getVolume());
            if (i >= 1) {
                canvas.drawLine((mGridWidth * (i - 1)) + mDrawWidth, getY(mSellData.get(i - 1).getVolume()),
                        (mGridWidth * i) + mDrawWidth, y, mSellLinePaint);
            }
            if (i != mSellData.size() - 1) {
                mSellPath.quadTo((mGridWidth * i) + mDrawWidth, y,
                        (mGridWidth * (i + 1)) + mDrawWidth, getY(mSellData.get(i + 1).getVolume()));
            }
            x = (mGridWidth * i) + mDrawWidth;
            mMapX.put((int) x, mSellData.get(i));
            mMapY.put((int) x, y);
            if (i == mSellData.size() - 1) {
                mSellPath.quadTo(mWidth, y, (mGridWidth * i) + mDrawWidth, mDrawHeight);
                mSellPath.quadTo((mGridWidth * i) + mDrawWidth, mDrawHeight, mDrawWidth, mDrawHeight);
                mSellPath.close();
            }
        }
        canvas.drawPath(mSellPath, mSellPathPaint);
    }
           

上面的是主要的繪制代碼塊,代碼邏輯就是先繪制出區域的邊線,同時通過path類記錄下相關的位置,周遊到最後一個對象時直接将path的路徑首位相連,再去繪制path所記錄的區域

繪制文案的代碼需要注意下

private void drawText(Canvas canvas) {
        float value;
        String str;
        for (int j = 0; j < mLineCount; j++) {
            value = mMaxVolume - mMultiple * j;
            str = getVolumeValue(value);
            canvas.drawText(str, mWidth, mDrawHeight / mLineCount * j + 30, mTextPaint);
        }
        int size = mBottomPrice.length;
        int height = mDrawHeight + mBottomPriceHeight / 2 + 10;
        if (size > 0 && mBottomPrice[0] != null) {
            String data = getValue(mBottomPrice[0]);
            canvas.drawText(data, mTextPaint.measureText(data), height, mTextPaint);
            data = getValue(mBottomPrice[1]);
            canvas.drawText(data, mDrawWidth - 10, height, mTextPaint);
            data = getValue(mBottomPrice[2]);
            canvas.drawText(data, mDrawWidth + mTextPaint.measureText(data) + 10, height, mTextPaint);
            data = getValue(mBottomPrice[3]);
            canvas.drawText(data, mWidth, height, mTextPaint);
        }
        if (mIsLongPress) {
            mIsHave = false;
            for (int key : mMapX.keySet()) {
                if (key == mEventX) {
                    mLastPosition = mEventX;
                    drawSelectorView(canvas, key);
                    break;
                }
            }
            //這裡這麼處理是保證滑動的時候界面始終有選中的感覺,不至于未選中的時候沒有展示,界面有閃爍感,體驗不好
            if (!mIsHave) {
                drawSelectorView(canvas, mLastPosition);
            }
        }
    }
           

以上是此自定義控件的主要代碼,項目已上傳至github,歡迎各位老鐵們star,fork,此庫定期更新一些自定義控件,互相交流學習。