這篇文章講述這樣繪製直方圖控件的思路,其實作任何事的前提條件就是分析它的一些特徵,理清思路。先給出繪製的直方圖控件圖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
在這個問題上想了好一下子,感受要是經過構建直角三角形來求出箭頭的座標很麻煩。其實咱們徹底能夠有更討巧的方式,好比畫一條直線,將其旋轉指定角度也是能夠實現的嘛!首先先從 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
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
繪製刻度分爲 x 軸的文字,和 y 軸的數字刻度。由於這裏只是舉例,至於你想繪製什麼無所謂,根據本身的實際需求來更改便可。繼續從 x 軸開始,首先咱們要計算出當前的 x 軸寬度能放下幾列直方圖, 每一列之間都有一個固定間距,間距的個數會比直方圖個數多1,這個你能夠在白紙上畫一下,擺放試試,你或許會忽略最後一列右側的間距,所以獲得相等。
第一步計算一下能擺放多少列,直方圖可用空間爲: x 軸的寬度 - 間距個數 * 間距,那麼可擺放數量爲: 直方圖可用空間 / 列數。
// 獲得直方圖列可繪製區域 totoalSpaces 爲 間距個數, space = 10, 固定間距
availableSpace = mXCoordinateWidth - totoalSpaces * space;
// 每一列的空間,即寬度.
availableColumnSpace = availableSpace / columns;
複製代碼
第二步,找出刻度的起始點座標,循環日後不斷繪製刻度,首先咱們假設當前有 4 列, 那麼初始點也就是第一列的中點,我這裏是以直方圖的中點畫刻度。給出一個草圖如圖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 所示
接下來是 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所示
其實經過前面的努力,已經將必要點都找到了,咱們只需知道當前直方圖在 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所示:
終於到尾聲了,累死去。繪製數字和文字就很簡單啦!由於須要的全部點都以給出,只須要在位置上繪製便可。先從 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
若是還須要將 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);
複製代碼
最後要說的就是,看不表明會,但願讀者能夠根據代碼理解並實現一遍, 鞏固下理解。固然大神就別噴我啦😭,爲了更好的適配,確定不能是有多少就展現多少的,應該考慮結合滑動來展現更多,值得說的事,該功能的實現並不是爲開源組件而來,只求講出本身的實現過程。由於考慮的地方不多,不一樣的數據類型,以及展現的樣式都沒加入進來。只看成學習思路,由於會了基礎思路。想要加上其餘功能就問題不大啦!