您的位置:首页 > 其它

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

2018-02-06 10:29 316 查看

前言

自定义 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,也就是设置了 MATCH_PARENT 或者设置了具体的值(比如12dp),还得继续取 result 和 size 中的最大值作为 View 最终的宽。

最后返回 result。

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

四、绘制

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

@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

LineProgressBar.java

使用实例

你可以通过以下方式关注我:

1. CSDN

2. 掘金

3. 个人博客
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: