Android自定義View - 繪製雷達圖
序言
做 Android 應用開發,介面自然是少不了的,它是最直接可被使用者感知的部分。每當看到手機上應用做出絢麗的畫面、巧妙的動畫,使用體驗就像把玩一件藝術品一般,真的令人讚歎!我的工作範圍很少涉及介面,所以對檢視方面瞭解不多。在網上找到了一份教程: GcsSloop 的 AndroidNote ,裡面對自定義 View 講得非常詳細,從基礎到進階,每個繪圖的 API 都有解釋,想要學習的朋友千萬不要錯過~
下面兩段摘自 GcsSloop 的 Android 筆記,分別總結了自定義 View 的分類和流程。
自定義 View 分類
PS:實際上 ViewGroup 是 View 的一個子類。
類別 | 繼承自 | 特點 |
---|---|---|
View | View SurfaceView 等 | 不含子 View |
ViewGroup | ViewGroup、xxLayout 等 | 包含子 View |
自定義 View 流程
步驟 | 關鍵字 | 作用 |
---|---|---|
1 | 建構函式 | 初始化 View |
2 | onMeasure | 測量 View 大小 |
3 | onSizeChanged | 確定 View 大小 |
4 | onLayout | 確定子 View 佈局(自定義 View 包含子 View 時有用) |
5 | onDraw | 實際繪製內容 |
6 | 提供介面 | 控制 View 或監聽 View 某些狀態 |
學習完 Path 的基本操作,GcsSloop 給我們留了一道作業題 ---- 繪製雷達圖,熟悉 Path 的使用。下面我們就按照步驟來做一下,其中涉及一些數學計算,看來演算法還是蠻重要的。
1. 建構函式,初始化 View
首先看成員變數的宣告,主要是畫筆、畫布的屬性(寬和高)、圖形的屬性(圈數、半徑等)。為了計算 cos 值,重溫了高中數學(笑哭 ing)
// 6條線上的點的 con 值,從 y 軸負方向開始畫線,即豎直的上方 private static final PointF[] UNIT_POINTS = { new PointF(0, -1), new PointF((float) (Math.cos(Math.PI / 6)), -(float) (Math.cos(Math.PI / 3))), new PointF((float) (Math.cos(Math.PI / 6)), (float) (Math.cos(Math.PI / 3))), new PointF(0, 1), new PointF(-(float) (Math.cos(Math.PI / 6)), (float) (Math.cos(Math.PI / 3))), new PointF(-(float) (Math.cos(Math.PI / 6)), -(float) (Math.cos(Math.PI / 3))), }; // 邊數 private static final int EDGE_COUNT = 6; private final ILogger log = LoggerFactory.getLogger("RadarView"); // 雷達線畫筆 private Paint mLinePaint; // 填色區畫筆 private Paint mAreaPaint; // 資料點畫筆 private Paint mPointPaint; // 畫布的寬 private int mWidth; // 畫布的高 private int mHeight; // 圈數,限制 3--5 圈 private int mLoop = 5; // 步長,限制 50--100 private float mStep = 100; // 「半徑」長度 private float mLength = mStep * mLoop; // 最外層端點的座標 private List<PointF> mEndPoints;
下面是構造方法,需要重寫三個方法,在這裡初始化畫筆和座標資料。
public RadarView(Context context) { this(context, null); } public RadarView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public RadarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initPaints(); initEndPoints(mLength); } // 初始化畫筆 private void initPaints() { mLinePaint = new Paint(); mLinePaint.setStyle(Paint.Style.STROKE); mLinePaint.setAntiAlias(true); mLinePaint.setColor(Color.BLACK); mAreaPaint = new Paint(); mAreaPaint.setStyle(Paint.Style.FILL); mAreaPaint.setAntiAlias(true); mAreaPaint.setColor(Color.BLUE); mAreaPaint.setAlpha(100); mPointPaint = new Paint(); mPointPaint.setAntiAlias(true); mPointPaint.setColor(Color.BLUE); mPointPaint.setStyle(Paint.Style.FILL); mPointPaint.setStrokeWidth(10); } // 新增最外層的6個端點 private void initEndPoints(float length) { mEndPoints = new ArrayList<>(EDGE_COUNT); PointF pointF; for (int i = 0; i < EDGE_COUNT; i++) { pointF = new PointF(); pointF.x = length * UNIT_POINTS[i].x; pointF.y = length * UNIT_POINTS[i].y; mEndPoints.add(pointF); } }
2. onSizeChanged,確定 View 的大小
由於我們要繪製的是簡單的 View,onMeasure 過程暫時不需要重寫。然後到了 onSizeChanged 方法,在這裡獲取當前 View 的寬高。關於 onSizeChanged,API 是這麼說的:在 layout 期間,當 View 的尺寸發生變化是被呼叫。所以這裡的寬高就是 View 測量後的真實寬高。
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); log.debug("onSizeChanged. w:{}, h:{}, oldW:{}, oldH:{}", w, h, oldw, oldh); mWidth = w; mHeight = h; }
3. onDraw,繪製實際的內容
由於我們的 View 不包含子 View,所以 onLayout 過程跳過,直接進行 onDraw 繪製。
我的思路和蜘蛛織網差不多:先從中心開始,畫出 6 條射線,作為圖形的骨架, 然後從外圈向內圈畫線,最後打點填色。要不然怎麼雷達圖又叫「蜘蛛網圖」呢 (~ o ~)~zZ
@Override protected void onDraw(Canvas canvas) { log.debug("onDraw. canvas:{}", canvas); super.onDraw(canvas); // 將座標原點移動到中心 canvas.translate(mWidth / 2, mHeight / 2); Path path = new Path(); // 先畫 6 條射線,這是基本骨架 int size = mEndPoints.size(); for (int i = 0; i < size; i++) { path.moveTo(0, 0); PointF endPoint = mEndPoints.get(i); path.lineTo(endPoint.x, endPoint.y); } canvas.drawPath(path, mLinePaint); path.reset(); // 再從外圈到內圈畫閉合線,一圈又一圈~ PointF firstPoint = mEndPoints.get(0); for (int i = mLoop; i >= 1; i--) { float rate = i / (float) mLoop; //log.info("rate:{}", rate); float firstX = firstPoint.x * rate; float firstY = firstPoint.y * rate; path.moveTo(firstX, firstY); for (int j = 1; j < size; j++) { PointF endPoint = mEndPoints.get(j); path.lineTo(endPoint.x * rate, endPoint.y * rate); } path.lineTo(firstX, firstY); } canvas.drawPath(path, mLinePaint); path.reset(); // 畫資料點 List<PointF> pointFs = generateFocused(); PointF firstF = pointFs.get(0); path.moveTo(firstF.x, firstF.y); for (PointF pointF : pointFs) { canvas.drawPoint(pointF.x, pointF.y, mPointPaint); path.lineTo(pointF.x, pointF.y); } // 畫填色區域 canvas.drawPath(path, mAreaPaint); path.reset(); } // 產生隨機資料點 private List<PointF> generateFocused() { List<PointF> focused = new ArrayList<>(mEndPoints.size()); PointF point; for (PointF pointF : mEndPoints) { point = new PointF(); float random = 0; // 為了讓區域好看,所以隨機合適的點 while (random < 0.2 || random > 0.8) { random = (float) Math.random(); } point.x = (random * pointF.x); point.y = (random * pointF.y); //log.debug("point. x:{}, y:{}", point.x, point.y); focused.add(point); } return focused; }
4. 提供介面,設定 View 的屬性
這裡主要提供了兩個對外的介面:設定雷達圖的圈數和步長,並且做了一些限制。設定完資料後,呼叫 invalidate 方法進行重繪,這樣就能提供多樣化的檢視啦~
// 設定圈數 public void setLoop(int loop) { if (loop < 3) { loop = 3; } else if (loop > 6) { loop = 6; } mLoop = loop; mLength = mLoop * mStep; setEndPoints(mLength); invalidate(); } // 設定步長 public void setStep(float step) { if (step < 50) { step = 50; } else if (step > 100) { step = 100; } mStep = step; mLength = mLoop * mStep; setEndPoints(mLength); invalidate(); } // 重新設定端點座標 private void setEndPoints(float length) { for (int i = 0, j = mEndPoints.size(); i < j; i++) { PointF pointF = mEndPoints.get(i); pointF.x = length * UNIT_POINTS[i].x; pointF.y = length * UNIT_POINTS[i].y; } }
5. 使用 View
直接建立 View,可以設定屬性,新增到介面即可~
LinearLayout container = (LinearLayout) findViewById(R.id.container); RadarView radarView = new RadarView(this); //radarView.setStep(80); //radarView.setLoop(5); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); container.removeAllViews(); container.addView(radarView, params);
一起來看下效果吧

效果圖
總結:
自定義 View 其實沒有那麼難,我們看到一些複雜的效果,往往不是幾十行程式碼能搞定的,可能就被嚇到了。把任務分解成小目標,設計良好的演算法,一步一步就能做出來。
【附錄】

資料圖