1,前兩天我們分析了Github開源的StepView 《自定義StepView實現個人信息驗證進度條》,這兩天想着想自己寫一個,so,就有了這一篇文章,不廢話,先看看實現的效果:
2,首先我們來看看我們常規的自定義view的基礎步驟吧
1,繼承View,重寫構造方法 2,自定義屬性 3,重寫onMeasure()測量控件高度 4,重寫onDraw()繪制子view
- 初步分析
首先根據我們的上面效果,可以看到,主要是由直線、圓環、下面的文字組成,所以我們打算使用這三種view組合來形成我們上面的效果
- 准備工作
①首先我們要提供一個裝置下面文字的集合texts,我們文字有文字的大小屬性mTextSize、正常文字顏色mColorTextDefault、文字被選中時的顏色mColorTextSelect,最后還有文字距離上面圓環的距離mMarginTop
②然后我們提供相關的圓環相關的屬性,圓的半徑mCircleRadius、圓環被選中的顏色mColorCircleSelect、圓環正常時的顏色mColorCircleDefault
③再看看我們鏈接圓弧之間的直線屬性,直線的長度mLineLength、直線的高度mLineHeight,顏色和我們圓環默認顏色相同,就不用重新定義了
④還有一些需要定義的屬性,例如當前被選中的位置mSelectPosition,每一個測量的TextView保存的Rect的集合mBounds,還有各種畫筆
所以我們就可以開始寫一寫代碼了,首先創建StepView繼承View,然后初始化數據,並測量TextView,將測量信息保存在mBounds集合中
public class SlideStepView extends View { //先分析我們這次需要哪些預備的屬性 //存放下面文字集合 private List<String> texts; //文字大小 private int mTextSize; //文字常規顏色 private int mColorTextDefault; //文字被選擇時候的顏色 private int mColorTextSelect; //圓和文字之間的距離 private int mMarginTop; //線段和圓圈常規的顏色 private int mColorCircleDefault; //圓圈被選中的的顏色 private int mColorCircleSelect; //中間線段的整個長度 private float mLineLength; //中間線段寬度 private int mLineHeight; //圓圈的半徑 private int mCircleRadius; //選中后藍色的寬度 private int mSelectCircleStroke; //當前選中的下標 private int mSelectPosition; //保存每個TextView的測量矩形數據 private List<Rect> mBounds; //各種畫筆 private Paint mTextPaint; private Paint mLinePaint; private Paint mCirclePaint; private Paint mCircleSelectPaint; public SlideStepView(Context context) { this(context, null); } public SlideStepView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public SlideStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //初始化數基本屬性 init(); } private void init() { //初始化數據源容器 texts = new ArrayList<>(); mBounds = new ArrayList<>(); //添加加數據 texts.add("訂單已支付"); texts.add("商家已接單"); texts.add("騎手已接單"); texts.add("訂單已送達"); //將當前選中為2 mSelectPosition = 1; mMarginTop = 20; mCircleRadius = 30; mSelectCircleStroke = 3; //初始化文字屬性 mColorTextDefault = Color.GRAY; mColorTextSelect = Color.BLUE; mTextSize = 20; mTextPaint = new Paint(); mTextPaint.setTextSize(mTextSize); mTextPaint.setColor(mColorTextDefault); mTextPaint.setAntiAlias(true); //初始化圓圈屬性 mColorCircleDefault = Color.argb(255, 234, 234, 234); mCirclePaint = new Paint(); mCirclePaint.setColor(mColorCircleDefault); mCirclePaint.setStyle(Paint.Style.FILL); mCirclePaint.setAntiAlias(true); //初始化被選中的圓圈 mColorCircleSelect = Color.BLUE; mCircleSelectPaint = new Paint(); mCircleSelectPaint.setColor(mColorCircleSelect); mCircleSelectPaint.setStyle(Paint.Style.FILL); mCircleSelectPaint.setAntiAlias(true); // mCircleSelectPaint.setStrokeWidth(mSelectCircleStroke); //設置線段屬性 mLineHeight = 5; mLinePaint = new Paint(); mLinePaint.setColor(mColorCircleDefault); mLinePaint.setStyle(Paint.Style.FILL); mLinePaint.setStrokeWidth(mLineHeight); mLinePaint.setAntiAlias(true); //測量TextView measureText(); } private void measureText() { for (int i = 0; i < texts.size(); i++) { Rect rect = new Rect(); mTextPaint.getTextBounds(texts.get(i), 0, texts.get(i).length(), rect); mBounds.add(rect); } } }
然后在onChangeSize中計算出mLineLength的長度(這里很簡單 getWidth() - paddingLeft -paddingRight -2*mCircleRadius),重寫onDraw()方法
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //計算線段整條線段長度(總控件寬度 - Padding - 最左邊和最右邊的兩個圓的直徑) mLineLength = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleRadius * 2; } /** * 繪制view * * @param canvas */ @Override protected void onDraw(Canvas canvas) { //繪制線條 canvas.drawLine(mCircleRadius, mCircleRadius, getWidth() - mCircleRadius, mCircleRadius, mLinePaint); //開是循環繪制view for (int i = 0; i < texts.size(); i++) { mTextPaint.setColor(mColorCircleDefault); if (mSelectPosition == i) { //繪制選中的圓圈 canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCircleSelectPaint); mTextPaint.setColor(mColorCircleSelect); } else { //繪制默中的圓圈 canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCirclePaint); } //繪制文字 int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop(); if (i == 0) { canvas.drawText(texts.get(i), 0, startTextY, mTextPaint); } else if (i == texts.size() - 1) { canvas.drawText(texts.get(i), getWidth() - mBounds.get(i).width(), startTextY, mTextPaint); } else { canvas.drawText(texts.get(i), mCircleRadius + ((mLineLength / (texts.size() - 1)) * i) - (mBounds.get(i).width() / 2), startTextY, mTextPaint); } } }
在布局文件引用
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20dip"> <com.qianmo.activitydetail.view.SlideStepView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#aaff0000"/> </LinearLayout>
這樣應該可以實現基本效果了,看一看我們實現的效果
- 重寫onMeasure,改變測量的高度
這里我們可以看到當我們設置我們控件的高度為wrap_content,控件缺填充了整個屏幕,這一點我們在之前的《onMeasure()源碼分析》寫過,沒有了解過的同學,大家可以去看一下,所以我們要修改onMeasure中的方法
/** * 重寫測量方式 * * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int height; if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { height = mMarginTop + 2 * mCircleRadius + mBounds.get(0).height(); //高度 Log.i("wangjitao:", "mMarginTop:" + mMarginTop + ",mCircleRadius:" + mCircleRadius + ",mBounds:" + mBounds.get(0).height() + ",height" + height); } //保存測量結果 setMeasuredDimension(widthSize, height); }
再看一下我們的運行效果
- 對canvas.drawText()方法進行理解
我們這時候將我們前面的init()方法中的mMarginTop修改為0,mMarginTop代表下面文字距離上面圓環的距離,設置為0的話就表示我們的文字的text剛好貼在這個圓環的下面,但是實際效果不是這個樣子的,看一下運行的效果
這里我們可以看到我們的文字和我們的圓弧重疊了,這是為什么呢? 我們的代碼邏輯也問題啊,為什么會出現這個問題呢?我們下來看一下下面這張text的展示圖就知道了
上面所有的屬性都被封裝在FontMetrics類中,通過它可以獲取並計算文本的寬高,大體翻譯一下,可能不准確; top:在一個大小確定的字體中,被當做最高字形,基線(base)上方的最大距離。 ascent:單行文本中,在基線(base)上方被推薦的距離。 descent:單行文本中,在基線(base)下方被推薦的距離。 bottom:在一個大小確定的字體中,被當做最低字形,基線(base)下方的最大距離。
這是我們自定義View中text的一些屬性,有人會問,樓豬啊 ,為什么要讓我們了解這個些知識呢?因為我們的上面出的重疊問題就是這一點的問題,在我們的正常思維的認知中我們的canvas.drawText的第三個參數是Y坐標的起始點,而我們上面的代碼Y坐標的計算方式是 startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop();我們的主觀思維也感覺沒問題,但是讓我們看一下canvas.drawText()方法的源碼
/** * Draw the text, with origin at (x,y), using the specified paint. The * origin is interpreted based on the Align setting in the paint. * * @param text The text to be drawn * @param x The x-coordinate of the origin of the text being drawn * @param y The y-coordinate of the baseline of the text being drawn * @param paint The paint used for the text (e.g. color, size, style) */ public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) { native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface); }
看到沒有“@param y The y-coordinate of the baseline of the text being drawn” 這個方法中我們的y參數表示我們的baseline,而不是我們之前的想當然的test的top屬性,所以我們要修改startTextY 的計算方式為
//這里要對基線進行理解 int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop(); //以前 Log.i("wangjitao", "以前:" + startTextY); //現在是這樣的,首先獲取基線對象 Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); startTextY = getHeight() - (int) fontMetrics.bottom;
ok,再看看我們的運行效果
沒什么問題了
- 重寫onTouch()方實現側滑更換當前選中位置
這個沒什么好講的,就是向左滑動和向右滑動改變當前選中位置而已,代碼如下:
private float downX; private float upX; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { //按下手指的時候記錄下按下的位置 case MotionEvent.ACTION_DOWN: Log.e("wangjitao", "手指按下: getX:" + downX); downX = event.getX(); break; case MotionEvent.ACTION_MOVE: Log.i("wangjitao", "手指滑動: "); break; case MotionEvent.ACTION_UP: upX = event.getX(); Log.e("wangjitao", "手指抬起: " + upX); if (downX - upX > 50) { downX = 0; upX = 0; //向左滑動 //判斷做滑動的時候當前選擇點時候在在初始狀態下 if (mSelectPosition != 0) { //更新view mSelectPosition--; } else { mSelectPosition = texts.size() - 1; } invalidate(); } else if (upX - downX > 50) { //向右滑動 downX = 0; upX = 0; //判斷做滑動的時候當前選擇點時候在最后一個點上 if (mSelectPosition != texts.size() - 1) { //更新view mSelectPosition++; } else { mSelectPosition = 0; } invalidate(); } else { downX = 0; upX = 0; } break; } return true; }
再把最后所有的代碼貼出來
package com.qianmo.activitydetail.view; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import java.util.ArrayList; import java.util.List; /** * Created by wangjitao on 2017/3/24 0024. * E-Mail:543441727@qq.com * 自定義view實現StepView的實現 */ public class SlideStepView extends View { //先分析我們這次需要哪些預備的屬性 //存放下面文字集合 private List<String> texts; //文字大小 private int mTextSize; //文字常規顏色 private int mColorTextDefault; //文字被選擇時候的顏色 private int mColorTextSelect; //圓和文字之間的距離 private int mMarginTop; //線段和圓圈常規的顏色 private int mColorCircleDefault; //圓圈被選中的的顏色 private int mColorCircleSelect; //中間線段的整個長度 private float mLineLength; //中間線段寬度 private int mLineHeight; //圓圈的半徑 private int mCircleRadius; //選中后藍色的寬度 private int mSelectCircleStroke; //當前選中的下標 private int mSelectPosition; //保存每個TextView的測量矩形數據 private List<Rect> mBounds; //各種畫筆 private Paint mTextPaint; private Paint mLinePaint; private Paint mCirclePaint; private Paint mCircleSelectPaint; public SlideStepView(Context context) { this(context, null); } public SlideStepView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public SlideStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //初始化數基本屬性 init(); } private void init() { //初始化數據源容器 texts = new ArrayList<>(); mBounds = new ArrayList<>(); //添加加數據 texts.add("訂單已支付"); texts.add("商家已接單"); texts.add("騎手已接單"); texts.add("訂單已送達"); //將當前選中為2 mSelectPosition = 1; mMarginTop = 0; mCircleRadius = 30; mSelectCircleStroke = 3; //初始化文字屬性 mColorTextDefault = Color.GRAY; mColorTextSelect = Color.BLUE; mTextSize = 20; mTextPaint = new Paint(); mTextPaint.setTextSize(mTextSize); mTextPaint.setColor(mColorTextDefault); mTextPaint.setAntiAlias(true); //初始化圓圈屬性 mColorCircleDefault = Color.argb(255, 234, 234, 234); mCirclePaint = new Paint(); mCirclePaint.setColor(mColorCircleDefault); mCirclePaint.setStyle(Paint.Style.FILL); mCirclePaint.setAntiAlias(true); //初始化被選中的圓圈 mColorCircleSelect = Color.BLUE; mCircleSelectPaint = new Paint(); mCircleSelectPaint.setColor(mColorCircleSelect); mCircleSelectPaint.setStyle(Paint.Style.FILL); mCircleSelectPaint.setAntiAlias(true); // mCircleSelectPaint.setStrokeWidth(mSelectCircleStroke); //設置線段屬性 mLineHeight = 5; mLinePaint = new Paint(); mLinePaint.setColor(mColorCircleDefault); mLinePaint.setStyle(Paint.Style.FILL); mLinePaint.setStrokeWidth(mLineHeight); mLinePaint.setAntiAlias(true); //測量TextView measureText(); } private void measureText() { for (int i = 0; i < texts.size(); i++) { Rect rect = new Rect(); mTextPaint.getTextBounds(texts.get(i), 0, texts.get(i).length(), rect); mBounds.add(rect); } } /** * 重寫測量方式 * * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int height; if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { height = mMarginTop + 2 * mCircleRadius + mBounds.get(0).height(); //高度 Log.i("wangjitao:", "mMarginTop:" + mMarginTop + ",mCircleRadius:" + mCircleRadius + ",mBounds:" + mBounds.get(0).height() + ",height" + height); } //保存測量結果 setMeasuredDimension(widthSize, height); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //計算線段整條線段長度(總控件寬度 - Padding - 最左邊和最右邊的兩個圓的直徑) mLineLength = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleRadius * 2; } /** * 繪制view * * @param canvas */ @Override protected void onDraw(Canvas canvas) { //繪制線條 canvas.drawLine(mCircleRadius, mCircleRadius, getWidth() - mCircleRadius, mCircleRadius, mLinePaint); //開是循環繪制view for (int i = 0; i < texts.size(); i++) { mTextPaint.setColor(mColorCircleDefault); if (mSelectPosition == i) { //繪制選中的圓圈 canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCircleSelectPaint); mTextPaint.setColor(mColorCircleSelect); } else { //繪制默中的圓圈 canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCirclePaint); } //繪制文字 //這里要對基線進行理解 int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop(); //以前 Log.i("wangjitao", "以前:" + startTextY); //現在是這樣的,首先獲取基線對象 Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); startTextY = getHeight() - (int) fontMetrics.bottom; Log.i("wangjitao", "現在:" + startTextY); if (i == 0) { canvas.drawText(texts.get(i), 0, startTextY, mTextPaint); } else if (i == texts.size() - 1) { canvas.drawText(texts.get(i), getWidth() - mBounds.get(i).width(), startTextY, mTextPaint); } else { canvas.drawText(texts.get(i), mCircleRadius + ((mLineLength / (texts.size() - 1)) * i) - (mBounds.get(i).width() / 2), startTextY, mTextPaint); } } } private float downX; private float upX; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { //按下手指的時候記錄下按下的位置 case MotionEvent.ACTION_DOWN: Log.e("wangjitao", "手指按下: getX:" + downX); downX = event.getX(); break; case MotionEvent.ACTION_MOVE: Log.i("wangjitao", "手指滑動: "); break; case MotionEvent.ACTION_UP: upX = event.getX(); Log.e("wangjitao", "手指抬起: " + upX); if (downX - upX > 50) { downX = 0; upX = 0; //向左滑動 //判斷做滑動的時候當前選擇點時候在在初始狀態下 if (mSelectPosition != 0) { //更新view mSelectPosition--; } else { mSelectPosition = texts.size() - 1; } invalidate(); } else if (upX - downX > 50) { //向右滑動 downX = 0; upX = 0; //判斷做滑動的時候當前選擇點時候在最后一個點上 if (mSelectPosition != texts.size() - 1) { //更新view mSelectPosition++; } else { mSelectPosition = 0; } invalidate(); } else { downX = 0; upX = 0; } break; } return true; } }
運行效果
- 添加自定義屬性
這里我們把好多控件的屬性都寫死了,我們可以用自定義屬性來實現布局文件中動態的改變的,不了解的同學可以看我之前的《深入了解自定義屬性》,這里就不一起寫了,See You····