安卓自定義view-繪製直方圖

這篇文章講述這樣繪製直方圖控件的思路,其實作任何事的前提條件就是分析它的一些特徵,理清思路。先給出繪製的直方圖控件圖1-1,這是我自定義繪製系列-自繪製控件初篇。因爲是自繪控件,這裏不會涉及到動畫,手勢事件處理。所以仍是比較簡單的,若是以爲直接好像對這種圖的實現沒有思路的,那你必定要看完。由於看完後你就會了😄😄。 先給出最終效果圖1-1算法

1-1

步驟分析

在解初中數學題的時候,通常都會先創建出直角座標系,而後在座標系上去找點。這裏也同樣,第一步先繪製座標系,以及刻度、箭頭。接下來就是繪製直方圖和數字與文字。canvas

座標系的繪製

首先咱們須要肯定直角座標系的寬高,這裏的寬高根據實際需求中去定,筆者這裏定義寬和高爲控件寬高的 2 / 3。代碼以下:bash

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // view 的寬度
        mWidth = w;

        // view 高度.
        mHeight = h;

        // x 軸座標的寬度
        mXCoordinateWidth = mWidth * 2 / 3;

        // y 軸座標的高度.
        mYCoordinateHeight = mHeight * 2 /3;
    }
複製代碼

爲何要在 onSizeChanged 中獲取寬高想必你們應該是知道的(當 view 的尺寸通過測量後獲得)。知道了寬高,剩下的就是找 x 軸的起點、終點,y 軸的起點、終點座標。先找 x 軸的起點, 因爲咱們須要座標系是在 view 居中的,那橫座標不就是 view 的寬度減去 x 軸寬度除以 2, 這樣左右兩邊的間隙都相同。縱座標是 view 的高度減去上邊距, 上邊距怎麼獲得呢? 跟前面的求橫座標是一個意思。 即 view 的高度減去 y 軸高度 除以2。 代碼以下:ide

view 的寬度減去 x 軸座標的寬度除以 2
startX = (mWidth - mXCoordinateWidth) / 2;

//  view 的高度減去上邊距
startY = mHeight - (mHeight - mYCoordinateHeight) / 2;
複製代碼

起點獲得了,終點就很簡單了。只須要改變橫座標的距離,縱座標不需改變,即起點的 x 座標加上 x 軸的寬度。代碼以下:學習

int endX = startX + mXCoordinateWidth;
// 縱座標不變。
 int endY = startY;
複製代碼

OK, 找到了起點和終點,這樣已經能夠肯定一條線段。動畫

// 繪製 x 軸座標.
        startX = (mWidth - mXCoordinateWidth) / 2;
        startY = mHeight - (mHeight - mYCoordinateHeight) / 2;
        int endX = startX + mXCoordinateWidth;
        int endY = startY;
  
        // 繪製 x 軸. 先不要糾結畫筆的定義,只想象經過筆畫出的而已。
        canvas.drawLine(startX, startY, endX, endY, mCoordinatePaint);
複製代碼

先不着急繪製箭頭,繼續繪製 y 軸。首先起點就是 x 軸的起點, 只須要找終點的座標,終點的橫座標爲 startX, 縱座標爲 view 的高度減去 y 軸的高度除以2。spa

// 繪製 y 座標.
        int yCoordinateStartX = startX;
        int yCoordinateStartY = startY;
                
        yCoordinateEndX = startX;       
        yCoordinateEndY = (mHeight - mYCoordinateHeight) / 2;
        canvas.drawLine(yCoordinateStartX, yCoordinateStartY, yCoordinateEndX, yCoordinateEndY, mCoordinatePaint);
複製代碼

最終的效果以下1-2 3d

1-2

繪製座標系箭頭

在這個問題上想了好一下子,感受要是經過構建直角三角形來求出箭頭的座標很麻煩。其實咱們徹底能夠有更討巧的方式,好比畫一條直線,將其旋轉指定角度也是能夠實現的嘛!首先先從 x 軸開始,由於箭頭有必定長度,若是直接繪製在末端點,會遮擋直方圖繪製區域,所以我這裏在原有 x 軸長度上額外加了 30。 第一步先將畫布的起點移到 endX + 30, endY, 再和原來的 x 軸末端點鏈接起來。rest

// 將原點畫布移動
 canvas.translate(endX + 30, endY);
// 鏈接末端點和原點. 這裏的 0, 0 爲移動後畫布的原點
 canvas.drawLine(-30, 0, 0, 0, mCoordinatePaint);

複製代碼

接下來將畫布旋轉 150度, 爲何是 150 度呢? 是由於我想獲得的是和 x 軸呈 30 度夾角, 度數爲正表示順時針旋轉。這樣就獲得了下方的直線,而後將畫布旋轉逆時針旋轉 150 度還原。再接着旋轉 210 度,繪製另外一條線段。也是比較好理解的,完整代碼以下:code

// x 軸箭頭
        // 將畫布狀態保存
        int layer = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        // 移動畫布原點
        canvas.translate(endX + 30, endY);
        // 鏈接原點和末端點
        canvas.drawLine(-30, 0, 0, 0, mCoordinatePaint);
  
       // 將畫布旋轉 150 度
        canvas.rotate(150);
        // 繪製線段
        canvas.drawLine(0, 0, 30, 0, mCoordinatePaint);

        // 將畫布旋轉還原
        canvas.rotate(-150);
       // 接着再旋轉 210 度
        canvas.rotate(210);
        
        // 繪製線段
        canvas.drawLine(0, 0, 30, 0, mCoordinatePaint);
        
        // 還原到指定畫布
        canvas.restoreToCount(layer);
複製代碼

效果圖1-3

1-3

x 軸的箭頭畫好了, y 軸其實也是一個原理。只是繪製第二個線段時沒有將畫布旋轉還原,而是繼續旋轉。也是同樣的意思。直接呈上代碼:

// y 軸箭頭
        int layer1 = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        canvas.translate(yCoordinateEndX, yCoordinateEndY - 60);
        canvas.drawLine(0, 60, 0, 0, mCoordinatePaint);
        canvas.rotate(60);
        canvas.drawLine(0, 0, 30, 0, mCoordinatePaint);
       // 繼續旋轉60度,即轉過的角度爲 120 度
        canvas.rotate(60);
        canvas.drawLine(0, 0, 30, 0, mCoordinatePaint);
        canvas.restoreToCount(layer1);

複製代碼

箭頭的最終效果,如圖1-4

1-4

繪製座標系刻度

繪製刻度分爲 x 軸的文字,和 y 軸的數字刻度。由於這裏只是舉例,至於你想繪製什麼無所謂,根據本身的實際需求來更改便可。繼續從 x 軸開始,首先咱們要計算出當前的 x 軸寬度能放下幾列直方圖, 每一列之間都有一個固定間距,間距的個數會比直方圖個數多1,這個你能夠在白紙上畫一下,擺放試試,你或許會忽略最後一列右側的間距,所以獲得相等。

第一步計算一下能擺放多少列,直方圖可用空間爲: x 軸的寬度 - 間距個數 * 間距,那麼可擺放數量爲: 直方圖可用空間 / 列數。

// 獲得直方圖列可繪製區域 totoalSpaces 爲 間距個數, space = 10, 固定間距
 availableSpace = mXCoordinateWidth - totoalSpaces * space;

// 每一列的空間,即寬度.
availableColumnSpace = availableSpace / columns;
複製代碼

第二步,找出刻度的起始點座標,循環日後不斷繪製刻度,首先咱們假設當前有 4 列, 那麼初始點也就是第一列的中點,我這裏是以直方圖的中點畫刻度。給出一個草圖如圖1-5:

1-5

上圖1-5中的 A 點爲咱們的起點,這點怎麼求呢? 首先 x 軸的起點已知,起點加上間距 space, 以及直方圖寬度的一半就能夠得出橫座標,縱座標爲 x 軸起點座標的縱座標。

// x 軸刻度橫座標
int xScalex = startX + space + availableColumnSpace / 2;
// 縱座標爲 x 軸起點座標的縱座標.
int xScaley = startY;
複製代碼

找到這一點後,是否是經過循環不斷向後移動便可。移動多少距離呢? 從圖1-5不難看出, 上一個點的橫座標加上 2 個直方圖寬度的一半以及一個間距。完整代碼以下:

@Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
  for (int i = 0; i < columns; i++) {
                HistogramBean bean =  datas.get(i);
                int height = bean.height;
                int max = bean.max;
                String color = bean.color;
                String columnName = bean.columnName;

                if (max == 0 || max < height) {
                    throw new IllegalStateException("最大值不能爲0");
                }
       
                // 繪製 x 軸刻度.
                drawXScaleValue(canvas, columnName);            
            }
}

private void drawXScaleValue(Canvas canvas, String columnName) {
        // 繪製 x 軸刻度.  xScaleStep 用來記錄下一次跳到的點.
        int xScalex = 0;
        if (xScaleStep > 0) {
            xScalex = xScaleStep;
        } else {
           // 找到第一個點.
            xScalex = startX + space + availableColumnSpace / 2;
        }
        int xScaley = startY;
    
      // 繪製一個 10  像素長度的刻度.
        int xScaleEndx = xScalex;
        int xScaleEndy = startY + 10;

        mPaint.setColor(Color.BLACK);
        canvas.drawLine(xScalex, xScaley, xScaleEndx, xScaleEndy, mPaint);
        
      // 記錄一個直方圖中點的橫座標
        xScaleStep = xScalex + 2 * availableColumnSpace / 2 + space;   
    }
複製代碼

如圖1-6 所示

1-6

接下來是 y 軸的刻度啦!我這裏將 y 軸刻度的繪製跟直方圖列數來對應的。即多少列就有多少刻度。它的算法是按着 y 軸的高度,根據直方圖的最大值進行縮放。說白了就是等分 y 軸,其具體的計算公式爲: 直方圖的最大值 / 列數 * y 軸高度 / max 獲得等分第一個高度。舉個列子,若是直方圖最大值是 100, 有 4 列, 假定 y 軸高度爲 300, 那麼 100 / 4 * 300 / 100 = 75。 說明這個刻度的高度爲 y 軸高度的 25, 那咱們就求出它的座標,後續的座標只要經過循環日後移動便可。它的座標爲 y 軸的終點縱座標加上 y 軸的高度減去前面公式所得的值。其完整代碼以下:

@Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
  for (int i = 0; i < columns; i++) {
                HistogramBean bean =  datas.get(i);
                int height = bean.height;
                int max = bean.max;
                String color = bean.color;
                String columnName = bean.columnName;

                if (max == 0 || max < height) {
                    throw new IllegalStateException("最大值不能爲0");
                }
       
               // 繪製 x 軸刻度.
                drawXScaleValue(canvas, columnName);

                // 根據列以及最大值來分配刻度值,能夠經過計算更換刻度的密度.
                // 這裏從 i + 1 開始,由於 i 是從 0 開始循環
                int yScaleValue = (i + 1) * max / columns * mYCoordinateHeight / max;
                drawYScaleValue(canvas, yScaleValue);        
            }
}

 private void drawYScaleValue(Canvas canvas, int yScaleValue) {
        // 繪製 y 軸刻度.
        // 根據列的數量進行刻度實現.  yScaleValue 這個值是每一個刻度的值,根據前面的公式計算獲得
        int yScaley = yCoordinateEndY + mYCoordinateHeight - yScaleValue;
        int yScalex = yCoordinateEndX;

        int yScaleEndx = yCoordinateEndX - 10;
        int yScaleEndy = yScaley;

        mPaint.setColor(Color.BLACK);
        canvas.drawLine(yScalex, yScaley, yScaleEndx, yScaleEndy, mPaint);
    }

複製代碼

如圖1-6所示

1-6

繪製直方圖

其實經過前面的努力,已經將必要點都找到了,咱們只需知道當前直方圖在 y 軸高度的值,這個高度爲: 直方圖的數值 * y 軸的高度 / 直方圖的最大值 max獲得。 獲得這個值能夠得出其距離頂部的距離,能夠得出該點的縱座標的值爲: y 軸終點縱座標 + y 軸高度 - 換算後的高度。

// 根據比列縮放獲得在 y 軸的高度.
int relalHeight = mYCoordinateHeight * height / max;
 // 距離定點的距離.
int top = (yCoordinateEndY + mYCoordinateHeight - relalHeight);

複製代碼

直方圖無非就是一個矩形,只要就出 left, top,right,bottom 就肯定矩形啦。其中很差求的也就是 top 啦!可是 top 在前面已被求得。那剩下的 left, right, bottom 就十分簡單啦!這裏的 left 移動規則和前面刻度的移動規則十分類似。若是理解了前面的移動規則,這裏就很容易理解。

@Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
  for (int i = 0; i < columns; i++) {
                HistogramBean bean =  datas.get(i);
                int height = bean.height;
                int max = bean.max;
                String color = bean.color;
                String columnName = bean.columnName;

                if (max == 0 || max < height) {
                    throw new IllegalStateException("最大值不能爲0");
                }

              // 繪製直方圖.
                drawHistogram(canvas, height, max, color);
       
               // 繪製 x 軸刻度.
                drawXScaleValue(canvas, columnName);

                // 根據列以及最大值來分配刻度值,能夠經過計算更換刻度的密度.
                // 這裏從 i + 1 開始,由於 i 是從 0 開始循環
                int yScaleValue = (i + 1) * max / columns * mYCoordinateHeight / max;
                drawYScaleValue(canvas, yScaleValue);        
            }
}

 private void drawHistogram(Canvas canvas, int height, int max, String color) {
        // 根據最大值的比列進行縮放.
        int relalHeight = mYCoordinateHeight * height / max;

        // 將上一次的距離加上固定間距 space 
        int left = startX + space + lastX;
        if (lastX > 0) {
            left = lastX + space;
        }
        int top = (yCoordinateEndY + mYCoordinateHeight - relalHeight);
        int right = (left + availableColumnSpace);
        int bottom = startY - 3;

       // 可經過外界設置直方圖的顏色
        if (!TextUtils.isEmpty(color)) {
            mPaint.setColor(Color.parseColor(color));
        }

        canvas.drawRect(left, top, right, bottom, mPaint);
        // 記錄上一次右邊的位置
        lastX = right;
    }

複製代碼

如圖1-7所示:

1-7

繪製數字和文字

終於到尾聲了,累死去。繪製數字和文字就很簡單啦!由於須要的全部點都以給出,只須要在位置上繪製便可。先從 x 軸開始。只是將文字繪製到刻度的下方並居中顯示。直接給出代碼。

// 繪製 x 軸刻度值.
        Rect columnNameRect = new Rect();
        mTextPaint.getTextBounds(columnName, 0, columnName.length(), columnNameRect);
        canvas.drawText(columnName,
                xScalex - columnNameRect.width() / 2,
                xScaleEndy + columnNameRect.height(),
                mTextPaint);

複製代碼

y 軸的數字刻度也是同樣的道理。

// 繪製 y 軸刻度值.
        Rect textRect = new Rect();
        mTextPaint.getTextBounds(yScaleValueStr, 0, yScaleValueStr.length(), textRect);
        canvas.drawText(yScaleValueStr,
                startX - textRect.width() - 20,
                yScaley + textRect.height() / 2,
                mTextPaint);
複製代碼

最終的效果圖 1-8

1-8

若是還須要將 0 刻度繪製,以下代碼便可。

// 補畫 y 軸 0 刻度
            int yScale0x = yCoordinateEndX;
            int yScale0y = mYCoordinateHeight + yCoordinateEndY;

            int yScaleEnd0x = yCoordinateEndX - 10;
            int yScaleEnd0y = yScale0y;
            canvas.drawLine(yScale0x, yScale0y, yScaleEnd0x, yScaleEnd0y, mPaint);
            // 繪製刻度值.
            String yScaleValueStr = "0";
            Rect textRect = new Rect();
            mTextPaint.getTextBounds(yScaleValueStr, 0, yScaleValueStr.length(), textRect);
            canvas.drawText(yScaleValueStr,
                    startX - textRect.width() - 20,
                    yScale0y + textRect.height() / 2,
                    mTextPaint);
複製代碼

最後要說的就是,看不表明會,但願讀者能夠根據代碼理解並實現一遍, 鞏固下理解。固然大神就別噴我啦😭,爲了更好的適配,確定不能是有多少就展現多少的,應該考慮結合滑動來展現更多,值得說的事,該功能的實現並不是爲開源組件而來,只求講出本身的實現過程。由於考慮的地方不多,不一樣的數據類型,以及展現的樣式都沒加入進來。只看成學習思路,由於會了基礎思路。想要加上其餘功能就問題不大啦!

相關文章
相關標籤/搜索