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 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 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);
            }
        }
    }

在布局文件引用 :



 
    

这样应该可以实现基本效果了,看一看我们实现的效果

   Android -- 自定义view之StepView_第1张图片

  • 重写onMeasure,改变测量的高度

  这里我们可以看到当我们设置我们控件的高度为wrap_content,控件缺填充了整个屏幕,这一点我们在之前的《onMeasure()源码分析》写过,没有了解过的同学,大家可以去看一下,所以我们要修改onMeasure中的方法

/**
     * ##重写onMeasure,改变测量的高度
     * 这里我们可以看到当我们设置我们控件的高度为wrap_content,控件缺填充了整个屏幕,
     * 这一点我们在之前的《onMeasure()源码分析》写过,没有了解过的同学,大家可以去看一下,
     * 所以我们要修改onMeasure中的方法
     * 

* ## 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); }

 再看一下我们的运行效果:

 Android -- 自定义view之StepView_第2张图片 

  • 对canvas.drawText()方法进行理解

  我们这时候将我们前面的init()方法中的mMarginTop修改为0,mMarginTop代表下面文字距离上面圆环的距离,设置为0的话就表示我们的文字的text刚好贴在这个圆环的下面,但是实际效果不是这个样子的,看一下运行的效果

   Android -- 自定义view之StepView_第3张图片

  这里我们可以看到我们的文字和我们的圆弧重叠了,这是为什么呢? 我们的代码逻辑也问题啊,为什么会出现这个问题呢?我们下来看一下下面这张text的展示图就知道了

  Android -- 自定义view之StepView_第4张图片

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,再看看我们的运行效果

  Android -- 自定义view之StepView_第5张图片

   没什么问题了

  • 重写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 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 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中的方法
     * 

* ## 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() 在控件大小发生改变时调用。所以这里初始化会被调用一次 *

* 作用:获取控件的宽和高度 *

*   然后在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····

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Android -- 自定义view之StepView)