Android 自定義 View 實現橫行時間軸

buzhiming發表於2019-12-08

本篇文章會說下如何使用並且要用麻煩的自定義 view 去實現時間軸效果,以及如何分析、實現自定義 view。 需要具備的知識:Paint、Canvas、自定義 view 的繪製流程。

原始碼地址

歡迎留言交流。

一、已經有很多 RecycleView 實現時間軸的例子,為何還要費勁的使用自定義 view 去實現時間軸?

首先看下最終想要的效果:

Android 自定義 View 實現橫行時間軸
根據上圖可以總結出以下幾點:

  1. 每個階段要顯示時間、階段名、狀態圖示、中間有虛線;
  2. 文字上下交錯顯示;
  3. 相鄰階段的文字在垂直方向上是可以相交的;
  4. 時間軸的個數不確定,但是要鋪滿螢幕並且不可滑動; 如果只實現上兩點的效果,使用 RecycleView 無疑是最好的選擇,但是要同時實現以上整個效果目前想到的最好的辦法就是使用自定義 view。

二、如何開始?

相信也有人跟我一樣,對自定義的繪製過程 view、canvas、path、paint 的使用有了解,但是真的要去寫自定義 view 確不知道從何開始,不知道第一步如何下手。我個人的總結就是:想要的太多,遲遲不動手,所以有想法一定要去動手試驗! 不要想著寫完第一次執行就是最終想展示的完美效果,而是要抱著整體拆分成不重複的小塊,然後去繪製重複塊,然後去一點點實現一步步完美的心態才能做出來。

所以首先要把想實現的 view 拆分成一個個小的可繪製的並且沒有重複的塊,以目前想實現的時間軸效果來說,最小可繪製無重複塊也就是隻包括一個時間結點的塊如圖:

Android 自定義 View 實現橫行時間軸
它包括:

  • 垂直居中的一條虛線;
  • 一個表示狀態的圖示;
  • 一個顯示時間的文字塊;
  • 一個顯示階段名的文字塊;

三、開始畫

有了上面的分析,接下來就要開始畫了。

1. 畫中間的線

首先畫虛線,如果虛線不知道怎麼畫,可以先畫一條實線,然後再去找畫虛線的方法。

使用 canvas 中畫線的方法 drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint),根據引數得知需知道線的起點與終點座標以及一個 paint 物件,因為是垂直居中且橫穿整個控制元件的直線所以可以確定兩個點的 y 座標是一樣的,也就是控制元件高的一半,起點的 x 座標為0,終點的 x 座標為控制元件的寬。也就是知道控制元件的寬和高之後就可以繪製出這條線。獲取控制元件的寬高,可以在 onMeasure 方法中獲取:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mViewWidth = MeasureSpec.getSize(widthMeasureSpec) - dip2px(mContext, mSafeDistance * 2);
    mViewHeight = MeasureSpec.getSize(heightMeasureSpec);
}
複製程式碼

畫線的程式碼(在 onDraw 方法中新增,下面其它的繪製方法同樣是在 onDraw 方法中新增):

// 定義畫筆,並設定相關屬性
Paint mLinePaint = new Paint();
mLinePaint.setColor(Color.parseColor("#999999"));
mLinePaint.setStrokeWidth(1);
mLinePaint.setStyle(Paint.Style.STROKE);
// 畫虛線
canvas.drawLine(0, mViewHeight / 2, mViewWidth, mViewHeight / 2, mLinePaint);
複製程式碼

2. 畫圖示

canvas 畫圖示的方法:drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint),根據方法的引數去分析如何準備值,這裡需要一個 bitmap 物件,起點座標以及 paint。bitmap 物件可以將資原始檔 drawable 轉為 bitmap 格式;座標就是控制元件的中心點。畫圖示的程式碼:

// 圖示 x,y 座標
Bitmap statusBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.ic_no_pass);
float bX = mViewWidth / 2;
// 垂直的中心點在圖示的頂部,所以要減去 bitmap 高的一半
float bY = mViewHeight / 2 - statusBitmap.getHeight() / 2f;
Paint mBitmapPaint = new Paint();
mBitmapPaint.setFilterBitmap(true);
canvas.drawBitmap(statusBitmap, bX, bY, mBitmapPaint);
複製程式碼

3. 畫文字

canvas 畫文字的方法:drawText(@NonNull String text, float x, float y, @NonNull Paint paint),依然是根據方法得知需要知道繪製的內容,開始的座標點以及 paint。當文字在圖示上方時,文字的 y 座標需要使用圖示的 y 座標減去文字到圖示的距離,x 座標同圖片的 x 座標一樣;當文字在圖示下方時,文字的 y 座標需要使用圖示的 y 座標加上文字到圖示的距離。畫文字的程式碼:

// 定義畫筆
Paint mDatePaint = new Paint();
mDatePaint.setColor(Color.parseColor("#666666"));
mDatePaint.setTextSize(dip2px(mContext, 12));
mDatePaint.setStyle(Paint.Style.FILL);
mDatePaint.setTextAlign(Paint.Align.CENTER);
mDatePaint.setAntiAlias(true);

Paint mNamePain = new Paint();
mNamePain.setColor(Color.parseColor("#666666"));
mNamePain.setTextSize(dip2px(mContext, 13));
mNamePain.setStyle(Paint.Style.FILL);
mNamePain.setTextAlign(Paint.Align.CENTER);
mNamePain.setAntiAlias(true);

// 定義座標變數
float dateX = bX + statusBitmap.getWidth() / 2f;
float dateY;
dateY = mViewHeight / 2 - dip2px(mContext, 19);

// 畫文字,在圖示上
canvas.drawText("有效時間", dateX, dateY, mNamePain);
canvas.drawText("09.27-09.29", dateX, dateY - dateTextHeight mDatePaint);

// 畫文字,在圖示下
dateY = mViewHeight / 2 + dip2px(mContext, 19);
canvas.drawText("09.27-09.29", dateX, dateY, mDatePaint);
canvas.drawText("有效時間", dateX, dateY + dateTextHeigh, mNamePain);
複製程式碼

4. 由區域性到整體

上面已經完成了只有一個時間點的繪製,接下來思考如果有多個時間點時如何繪製。只有一個時間點時計算座標是以控制元件的寬高進行計算,那麼當有兩個時間點的時候需要首先把控制元件均分成兩部分,然後在均分的部分中計算對應的座標,完成繪製。當有三個時間點的時候需要均分為三部分,然後在各自的部分計算對應的座標,完成繪製。所以得到不論時間點的個數有多少繪製的方法不會改變,需要改變的是繪製時候用到的點的座標。其實已經可以看出,當多個點的時候需要迴圈一下,程式碼如下:

// 得到多個點時,其中每個部分的寬,itevW 也就等同與上面只有一個時間點時控制元件的寬
float itemW = mViewWidth / mDataList.size();
for (int i = 0; i < mDataList.size(); i++) {
    // 完成相關計算、繪製
}
複製程式碼

5. 完善

到這裡,整個分析及繪製就差不多要結束了。

繪製虛線的方法:mLinePaint.setPathEffect(new DashPathEffect(new float[]{10, 10}, 0));

繪製虛線時遇到一個問題,在手機上不顯示虛線效果,後來查到需要關閉 view 層的硬體加速:setLayerType(View.LAYER_TYPE_SOFTWARE, null);

繪製文字時的中心點計算需要注意下,可以 參考

paint、bitmap 等變數的初始化建議放到初始化方法中去做,不建議在 onDraw 方法中做。

四、總結

關於自定義 view 一定要多看,多嘗試。

相關文章