自定义 View 实战(一)做一个简单的进度条

前言

自定义 View 是每个 Android 程序员走向高级必经之路,本篇通过实现一个非常简单的自定义 View ,来简单了解下自定义 View 的流程。(最后会给出源码)

先看下效果:

自定义LineProgressBar

录制的 gif 可能看不清,欢迎去 Github下载项目运行查看。

一、分析需求

这个 View 是我前段时间做公司项目的时候写的,要求的功能比较简单:

  • 根据给出的百分比显示进度条
  • 中间一直存在的线条
  • 进度条的颜色
  • 线条的颜色
  • 进度条是否有动画效果

需求简单,所以实现起来也很简单的,接下来就一步一步的实现。

二、定义属性并获取

根据上面的分析,我们在 res/values 下面新建文件 attrs.xml,定义我们需要的属性如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="LineProgressBar">
        <!--进度条颜色-->
        <attr name="progress_color" format="color" />
        <!--中间线的颜色-->
        <attr name="progress_line_color" format="color" />
        <!--进度条的值-->
        <attr name="progress" format="integer" />
        <!--是否有动画效果-->
        <attr name="is_smooth_progress" format="boolean" />
    </declare-styleable>
</resources>

上面的属性的定义是在布局文件中使用的。

然后我们需要在自定义的 View 里面对应获取 xml 中定义的属性。

由于我们定义的是进度条,需要有最大值,根据百分比来显示进度。

新建 LineProgressBar 继承于 View:

/**
 * @author smartsean
 */
public class LineProgressBar extends View {
    //进度条的最大值
    private static final int MAX_PROGRESS = 100;
    //默认中间线颜色
    private static final int DEFAULT_LINE_COLOR = Color.parseColor("#e6e6e6");
    //默认进度条颜色
    private static final int DEFAULT_PROGRESS_COLOR = Color.parseColor("#71db77");

    /**
     * progress底部线的画笔
     */
    private Paint linePaint;
    /**
     * progress画笔
     */
    private Paint progressPaint;
    /**
     * progress底部线的颜色
     */
    private int lineColor;
    /**
     * progress的颜色
     */
    private int progressColor;
    /**
     * 进度值 百分比
     */
    private float progress;
    /**
     * 是否平滑显示progress
     */
    private boolean isSmoothProgress;

    public LineProgressBar(Context context) {
        this(context, null);
    }

    public LineProgressBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LineProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

}

接下来,我们需要在 init(context, attrs) 里面获取 attrs.xml 中定义的属性,并进行一些初始化。

    /**
     * 初始化参数
     */
    private void init(Context context, AttributeSet attrs) {
        TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.LineProgressBar);
        lineColor = attributes.getColor(R.styleable.LineProgressBar_progress_line_color, DEFAULT_LINE_COLOR);
        progressColor = attributes.getColor(R.styleable.LineProgressBar_progress_color, DEFAULT_PROGRESS_COLOR);
        progress = attributes.getInteger(R.styleable.LineProgressBar_progress, 0) / MAX_PROGRESS;
        isSmoothProgress = attributes.getBoolean(R.styleable.LineProgressBar_is_smooth_progress, true);
        attributes.recycle();
        initializePainters();
    }

三、测量

重写 onMeasure 方法,测量 View 的真实宽高

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false));
    }

    private int measure(int measureSpec, boolean isWidth) {
        int result;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom();
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight();
            result += padding;
            if (mode == MeasureSpec.AT_MOST) {
                if (isWidth) {
                    result = Math.max(result, size);
                } else {
                    result = Math.min(result, size);
                }
            }
        }
        return result;
    }

分析之前先看几个概念: 一个 MeasureSpec 被分为两部分

  • mode 用来存储测量模式,由 MeasureSpec 的高两位存储
  • size 用来存储大小,由 MeasureSpec 的低30位存储

mode 模式分为三种:

  • UNSPECIFIED 未指定模式,View 想多大就多大,父容器不做限制,一般用于系统内部的测量
  • AT_MOST :最大模式,对应于 layout_width 或者 layout_height 设置为 wrap_content ,子 View 的最终大小是最终父 View 指定的 size 值,并且子 View 的最终大小不能大于这个 size 值。
  • EXACTLY 精确模式,对应于layout_width 或者 layout_height 设置为 match_parent 或者 具体的值(比如12dp),父容器测量出 View 所需的大小,也就是 size 的值。

接下来开始具体的测量,首先 measure 是一个公共方法,用来测量 View 宽和高。

这里以测量宽为例分析下 measure 方法:

int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);

MeasureSpec.getMode(measureSpec) 获得测量模式 mode
MeasureSpec.getSize(measureSpec) 大小 size。

int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom(); 

如果是测量宽,获取左侧和右侧的 padding 之和赋值给 padding。
如果是测量高,获取底部和顶部的 padding 之和赋值给 padding。

if (mode == MeasureSpec.EXACTLY) {
    result = size;
} else {
    result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight();
    result += padding;
    if (mode == MeasureSpec.AT_MOST) {
        if (isWidth) {
            result = Math.max(result, size);
        } else {
            result = Math.min(result, size);
        }
    }
}

如果测量值为精确模式 MeasureSpec.EXACTLY ,View 已经明确了自己的大小,那么直接返回 size。

如果测量模式是 UNSPECIFIED 或者 AT_MOST,取 getSuggestedMinimumWidth ,那么这个 getSuggestedMinimumWidth() 是什么呢?

来看下源码:

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

可以看到,先判断了背景 mBackground 是不是为空

  • 如果为空,就直接取 mMinWidth 的值,也就是对应 xml 中的 android:minWidth
  • 如果不为空,取 mMinWidth 和 mBackground 背景的最大宽度。

具体到本实例就是说:如果你设置了 wrap_content,

  • 设置了 minWidth ,没有设置背景,就按照 minWidth
  • 设置了 minWidth ,有设置背景,就取 minWidth 和 mBackground.getMinimumWidth() 的最大值
  • 没设置 minWidth ,没有设置背景,就取 minWidth 的默认值0.
  • 没设置了 minWidth ,有设置背景,就取 minWidth(默认值为0) 和 mBackground.getMinimumWidth() 的最大值

mBackground.getMinimumWidth() 就是背景 Drawable 的原始宽高。

有可能我们的 View 设置了 leftPadding 或者 rightPadding,然后再把上面计算的 padding 加到 result 上。

如果测量模式是 UNSPECIFIED ,那么本次测量就结束了。

但是如果测量模式是 AT_MOST,也就是设置了 WRAP_CONTENT,还得继续取 result 和 size 中的最大值作为 View 最终的宽。

最后返回 result。

View 的宽度测量完毕,高度测量和宽度测量差不多,可以仔细体会下。

四、布局

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
}

布局一般是需要我们重写 View 的 onLayout 方法,正常情况下,我们最终的布局尺寸等于我们通过测量得到的尺寸,没有特殊需求是不用处理的。

也就是说,大多数情况下:

getMeasuredWidth() 等于 getWidth();
getMeasuredHeight() 等于 getHeight();

五、绘制

绘制就比较简单了,只有两行代码:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawLine(0, getHeight() / 2.0f, getWidth(), getHeight() / 2.0f, linePaint);
    canvas.drawRoundRect(new RectF(0, 0, progress * getWidth(), getHeight()), getHeight() / 2.0f, getHeight() / 2.0f, progressPaint);
}

先来看第一行代码:

canvas.drawLine(0, getHeight() / 2.0f, getWidth(), getHeight() / 2.0f, linePaint);

通过 canvas 的 drawLine 方法画出中间的线。

前两个参数表示画线的起点

  • 0代表从 x 轴的起点开始画
  • getHeight() / 2.0f 表示从 y 轴向下 getHeight() / 2.0f 开始画。

后两个参数表示画线的终点

  • getWidth() 也就是整个 View 的宽度
  • getHeight() / 2.0f 依旧表示从 y 轴向下 getHeight() / 2.0f 开始画。

再来看第二行代码:

canvas.drawRoundRect(new RectF(0, 0, progress * getWidth(), getHeight()), getHeight() / 2.0f, getHeight() / 2.0f, progressPaint);

第一个参数:

//表示整个View的所在的矩形
new RectF(0, 0, progress * getWidth(), getHeight())

第二个参数和第三个参数都是 getHeight() / 2.0f ,表示的是 View 在 x 轴和 y 轴上的圆角半径。

最后一个参数是画进度条的画笔。

只有这两行代码就可以实现整个自定义 View 的绘制。

整个核心绘制已经结束了。

但是我们需要在 Java 代码中动态的更改 View 的进度,所以需要在 View 添加 setProgress 方法如下:

/**
 * 设置进度
 *
 * @param pProgress
 */
public void setProgress(float pProgress) {
    if (pProgress > 1) {
        pProgress = 1;
    } else if (pProgress < 0) {
        pProgress = 0;
    }
    if (isSmoothProgress) {
        smoothRun(this.progress, pProgress);
    } else {
        this.progress = pProgress;
        invalidate();
    }
}

很简单,如果大于 1,那么就绘制最大进度值 1,
如果小于 0,就绘制最小进度值 0.
如果在 0 和 1 之间:
- 设置带动画的滑动就调用 smoothRun 这个方法。
- 如果不带动画,就直接 invalidate 刷新。

看下 smoothRun 方法:

/**
 * 设置平滑滑动
 *
 * @param currentProgress
 * @param targetProgress
 */
private void smoothRun(float currentProgress, float targetProgress) {
    ValueAnimator valueAnimator = ValueAnimator.ofFloat(currentProgress, targetProgress);
    valueAnimator.setTarget(this.progress);
    valueAnimator.setDuration(1000);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            progress = (float) animation.getAnimatedValue();
            invalidate();
        }
    });
    valueAnimator.start();
}
  1. 首先根据传入的当前的进度值和要显示的进度值创建 ValueAnimator 对象。
  2. 设置操作的动画属性为 this.progress。
  3. 设置动画时长 1000 毫秒。
  4. 然后监听动画过程,动态的给 progress 赋值,不断的刷新 View ,达到动画效果。
  5. 最后再开始动画。

最后

前面就是绘制一个简单的自定义 View 的全部过程,虽然代码量不多,但是要考虑的东西还是不少的。接下来还会继续把项目中用到的自定义 View 分享出来。

代码地址如下:
attrs.xml

LineProgressBar.java

使用实例

你可以通过以下方式关注我:
1. CSDN
2. 掘金
3. 个人博客

猜你喜欢

转载自blog.csdn.net/Sean_css/article/details/79267932