自定义 View 实战(一)做一个简单的进度条
前言
自定义 View 是每个 Android 程序员走向高级必经之路,本篇通过实现一个非常简单的自定义 View ,来简单了解下自定义 View 的流程。(最后会给出源码)
先看下效果:
录制的 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();
}
- 首先根据传入的当前的进度值和要显示的进度值创建 ValueAnimator 对象。
- 设置操作的动画属性为 this.progress。
- 设置动画时长 1000 毫秒。
- 然后监听动画过程,动态的给 progress 赋值,不断的刷新 View ,达到动画效果。
- 最后再开始动画。
最后
前面就是绘制一个简单的自定义 View 的全部过程,虽然代码量不多,但是要考虑的东西还是不少的。接下来还会继续把项目中用到的自定义 View 分享出来。
代码地址如下:
attrs.xml
你可以通过以下方式关注我: