1. 程式人生 > >Android -- 自定義view之StepView

Android -- 自定義view之StepView

先看看實現的效果:

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();
    }
 
/**
* mTextPaint.getTextBounds 把textview 的寬度和高度,當做一個矩形,放在mBounds集合中,
* 用來獲取字型的高度和寬度;
*
*/
    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()方法 


/**
*   
*  onSizeChanged() 在控制元件大小發生改變時呼叫。所以這裡初始化會被呼叫一次
*  
*  作用:獲取控制元件的寬和高度的好時機
*
*
*/
@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中的方法

/**
     * ##重寫onMeasure,改變測量的高度
     * 這裡我們可以看到當我們設定我們控制元件的高度為wrap_content,控制元件缺填充了整個螢幕,
     * 這一點我們在之前的《onMeasure()原始碼分析》寫過,沒有了解過的同學,大家可以去看一下,
     * 所以我們要修改onMeasure中的方法
     * <p>
     * ##  MeasureSpec.EXACTLY   按照自定義view的實際的高度來測量
     *
     * @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的展示圖就知道了

  

1

2

3

4

5

上面所有的屬性都被封裝在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;

另外要知道的知識點:

       Paint  mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setTextSize(textSize);
        mPaint.setTextAlign(Paint.Align.CENTER);

        final float ascent = mPaint.ascent();
        final float descent = mPaint.descent();

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.githang.stepview.demo;

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 dingxujun on 2018/12/10.
 *
 * @project StepView-master
 */
public class MyStepView extends View {

    public static final String TAG = MyStepView.class.getName();
    //先分析我們這次需要哪些預備的屬性

    //存放下面文字集合
    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 MyStepView(Context context) {
        this(context, null);
    }

    public MyStepView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyStepView(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);
        }
    }

    /**
     * ##重寫onMeasure,改變測量的高度
     * 這裡我們可以看到當我們設定我們控制元件的高度為wrap_content,控制元件缺填充了整個螢幕,
     * 這一點我們在之前的《onMeasure()原始碼分析》寫過,沒有了解過的同學,大家可以去看一下,
     * 所以我們要修改onMeasure中的方法
     * <p>
     * ##  MeasureSpec.EXACTLY   按照自定義view的實際的高度來測量
     *
     * @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);
    }


    /**
     * onSizeChanged() 在控制元件大小發生改變時呼叫。所以這裡初始化會被呼叫一次
     * <p>
     * 作用:獲取控制元件的寬和高度
     * <p>
     *   然後在onChangeSize中計算出mLineLength的長度
     * (這裡很簡單 getWidth() - paddingLeft -paddingRight -2*mCircleRadius),重寫onDraw()方法
     *
     * @param w
     * @param h
     * @param oldw
     * @param oldh
     */
    @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;
        Log.e(TAG, "線段長度" + mLineLength);
    }

    /**
     * 繪製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:
                downX = event.getX();
                Log.e("wangjitao", "手指按下:  getX:" + downX);
                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····