zoukankan      html  css  js  c++  java
  • Android -- 打造我们的StepView

    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····

  • 相关阅读:
    【?】Hello。。。
    【Calc】对于‘精分’的研究
    [AFO]记五年oi生涯及CSP2019复赛游记
    CF#579div.3
    「BZOJ1827」奶牛大集会
    「BZOJ2821」作诗 && 「BZOJ2724」蒲公英
    「BZOJ4576」262144
    「Bzoj5055」膜法师
    「51nod1689」逛街
    「51nod1681」公共祖先 &&「51nod2553」双重祖先
  • 原文地址:https://www.cnblogs.com/wjtaigwh/p/6612572.html
Copyright © 2011-2022 走看看