关于自定义View,我们前面已经有三篇文章在介绍了,如果筒子们还没阅读,建议先看一下,分别是android自定义View之钟表诞生记、android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检索、android自定义View之NotePad出鞘记。这三篇文章中所述的自定义View都还是比较简单的,我们只使用了其中的绘图API,那么今天我们来看看自定义ProgressBar,在这个过程中,我们顺便来看看自定义View中两个非常关键的方法,一个是View的测量,还有一个是自定义属性。OK,废话不多说,先来看一张效果图:
OK,动手吧。
1.准备工作
写一个类继承自View,先来声明变量,看看我们需要哪些变量:
/**
* View默认的宽
*/
private static final int DEFAULTWIDTH = 100;
/**
* View默认的高度
*/
private static final int DEFAULTHEIGHT = 100;
/**
* 外层圆圈的线条宽度
*/
private int stoke = 7;
/**
* 外层圆圈的线条颜色
*/
private int circleColor = Color.BLACK;
/**
* 内外圆圈之间的间距
*/
private int padding = 20;
/**
* 内层实体圆的颜色
*/
private int sweepColor = Color.RED;
/**
* 开始绘制的角度
*/
private int startAngle = -90;
/**
* 已经绘制的角度
*/
private int sweepAngle = 0;
/**
* 每次增长的度数
*/
private int sweepStep = 1;
/**
* 画笔
*/
private Paint paint;
/**
* 绘制扇形需要的矩形
*/
private RectF rectF;
OK,就这么几个变量,都很简单。接下来我们再看看构造方法,在构造方法中我只需要对画笔进行简单的初始化即可,如下(请大家注意构造方法的调用方式,如有疑问请查看之前自定义View的博客):
public MyProgressBar(Context context) {
this(context, null);
} public MyProgressBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
} public MyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
paint.setAntiAlias(true);
}
2.View绘制
接下来我们来看看View的绘制,大家看上面的效果图就知道,我们这里的绘制一共有两部分,一部分是外部圆环的绘制,还有一部分是内部扇形的绘制,那我们一步一步来:
1.绘制圆环
圆环的绘制很简单,直接看代码:
//设置圆环的颜色
paint.setColor(circleColor);
//设置圆环的宽度
paint.setStrokeWidth(stoke);
//设置绘制模式为描边
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, (float) (getWidth() / 2 - Math.ceil(stoke / 2.0)), paint);
这里有一个值得说一下的地方,drawCircle方法有四个参数,前两个是圆环的中心点的坐标,第三个是半径,本来半径是View宽度的一半即可,但是由于系统在绘制的过程中圆环线条宽度的一半算在半径中,另一半不算在半径中,所以这里的半径我们要适当缩小。
2.绘制扇形
扇形的绘制需要我们先构造一个RectF类(当然这个并不是必须的操作),然后就可以开始绘制了,如下:
//设置扇形的颜色
paint.setColor(sweepColor);
//设置扇形的绘制风格
paint.setStyle(Paint.Style.FILL);
//构造一个RectF 出来,扇形绘制在该RectF中
rectF = new RectF(padding, padding, getWidth() - padding, getHeight() - padding);
//绘制扇形
//四个参数分别是扇形所在的矩形,开始绘制的角度,需要绘制的角度,扇形是否和矩形共用一个中心点,画笔
canvas.drawArc(rectF, startAngle, sweepAngle, true, paint);
//增加要绘制的角度
sweepAngle += sweepStep;
//如果要绘制的角度大于360度,就从0重新开始绘制
sweepAngle = sweepAngle > 360 ? 0 : sweepAngle;
invalidate();
OK,做好上面这几步之后,我的一个自定义ProgressBar基本上就显示出来了。这个时候我只需要在布局文件中添加上这个自定义控件即可,如下:
<lenve.myprogressbar.MyProgressBar
android:layout_width="128dp"
android:layout_height="128dp"/>
但是我在布局文件中添加自定义控件的时候只能给它一个固定的宽和高,如果给一个wrap_content或者match_parent那么我的自定义ProgressBar就会显示不正常,这个问题该怎么解决呢?这里就涉及到了一个新的知识,那就是View的测量。
3.View测量
当我们自定义一个View的时候,除了重写onDraw方法之外,还有一个方法有时候也需要我们重写,那就是onMeasure,onMeasure方法接收两个参数,分别是widthMeasureSpec和heightMeasureSpec,这两个参数我们称作测量规格,它们是一个32位的整型数据,这个数据中高2位表示View的测量模式,低30位表示View的测量值,测量模式分为3种,分别是:
1.EXACTLY:精确模式,对应我们在布局文件中设置宽高时给一个具体值或者match_parent
2.AT_MOST:最大值模式:对应设置宽高时给一个wrap_content
3.UNSPECIFIED:这种测量模式多用在ScrollView中
OK,了解了这些之后,接下来我们就来看看怎么样从widthMeasureSpec和heightMeasureSpec中提取出来宽高对应的测量模式与测量值。在MeasureSpec类中提供了两个静态方法,分别是getMode和getSize,只要我们将宽高的测量规格传递进去就可以获取它的测量模式和测量值。如下:
//获取宽的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//获取宽的测量值
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//获取高的测量模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//获取高的测量值
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
拿到宽高的测量模式和测量值之后,我们就可以做一个简单的处理了,大家已经知道测量模式一共分为三种,如果用户明确指定了View的宽和高那我就不去管它,如果用户给了一个wrap_content或者使用了第三种测量模式的话,那我就给View一个默认的宽和高,OK,就这么一个简单的逻辑,我们来看看代码:
switch (widthMode) {
case MeasureSpec.EXACTLY:
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
//如果宽为wrap_content,则给定一个默认值
widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULTWIDTH, getResources().getDisplayMetrics());
break;
}
switch (heightMode) {
case MeasureSpec.EXACTLY:
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULTHEIGHT, getResources().getDisplayMetrics());
break;
}
OK,最后,由于我的ProgressBar是绘制一个圆,因此View的宽高必须是相同的,所以再添加一行代码:
widthSize = heightSize = Math.min(widthSize, heightSize);
OK,至此,我的View的宽高都确定下来了,最后我只需要调用setMeasuredDimension方法,告诉系统我的测量结果即可,如下:
setMeasuredDimension(widthSize, heightSize);
所以,一个完整的onMeasure方法应该是下面这个样子:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//获取宽的测量值
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//获取高的测量模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//获取高的测量值
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
switch (widthMode) {
case MeasureSpec.EXACTLY:
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
//如果宽为wrap_content,则给定一个默认值
widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULTWIDTH, getResources().getDisplayMetrics());
break;
}
switch (heightMode) {
case MeasureSpec.EXACTLY:
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULTHEIGHT, getResources().getDisplayMetrics());
break;
}
widthSize = heightSize = Math.min(widthSize, heightSize);
//设置测量结果
setMeasuredDimension(widthSize, heightSize);
}
OK,从此以后,你就可以在布局文件中给ProgressBar设置任意的宽和高了。
4.自定义属性
做完上面这几步,我的自定义ProgressBar已经完成的差不多了,现在我如果想要修改圆环的颜色,圆环的线条的宽度,扇形的颜色等等这些属性的话只能在代码中修改,可是如果我想要在布局文件中来配置这些颜色,然后在代码中读取这些颜色再设置给paint又该怎么办呢?这里就涉及到我们的自定义属性了。OK,那么接下来我们就来看看自定义属性。
自定义属性需要我们首先在res/values文件夹中添加attrs文件(该文件名可以任意取,约定俗成取attrs),在attrs文件中来生命你要设置的属性,如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyProgressBar">
<attr name="circleColor" format="color|reference"/>
<attr name="sweepColor" format="color|reference"/>
<attr name="stroke" format="dimension|reference"/>
<attr name="sweepStep" format="integer|reference"/>
<attr name="startAngle" format="integer|reference"/>
<attr name="mypb_padding" format="dimension|reference"/>
</declare-styleable>
</resources>
首先我们要声明declare-styleable,给它取一个名字,这个name可以任意取,但是强烈建议取自定义View的类名,因为只有取类名,一会你在布局文件中添加这些属性时系统才会有提示。OK,里面的attr节点就是我们定义的一个个的属性了,name表示属性的颜色,format表示属性的取值,format取值主要有如下几种:
1. boolean 属性取值为boolean类型
2. string 属性取值为文本类型
3. color 属性取值为颜色类型
4. dimension 属性值为尺寸
5. enum 属性取值是枚举类型,例如:LinearLayout中的android:orientation="horizontal"属性
6. flag 属性取值进行或运算,比如android:layout_gravity="left|bottom"
7. fraction 属性取值为小数
8. float 属性取值为浮点数
9. integer 属性取值为整数
10. reference 属性取值可以引用一个值
OK,这一部分的工作完成之后,接下来我们就可以在布局文件中设置属性了,如下:
<lenve.myprogressbar.MyProgressBar
app:circleColor="#10ff03"
app:mypb_padding="30dp"
app:startAngle="0"
app:stroke="10dp"
app:sweepColor="#0184ff"
app:sweepStep="2"
android:layout_width="128dp"
android:layout_height="128dp"/>
OK,但是光这样肯定不行,我只是在布局文件中设置了,代码里又该怎么样来获取布局文件中设置的值呢?修改构造方法如下:
public MyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
paint.setAntiAlias(true);
//读取布局文件中设置的属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyProgressBar);
//读取布局文件中定义的颜色值,第二个参数为默认值(如果布局文件中未设置该属性时使用)
circleColor = ta.getColor(R.styleable.MyProgressBar_circleColor, this.circleColor);
sweepColor = ta.getColor(R.styleable.MyProgressBar_sweepColor, this.sweepColor);
startAngle = ta.getInt(R.styleable.MyProgressBar_startAngle, this.startAngle);
sweepStep = ta.getInt(R.styleable.MyProgressBar_sweepStep, this.sweepStep);
stroke = (int) ta.getDimension(R.styleable.MyProgressBar_stroke, stroke);
padding = (int) ta.getDimension(R.styleable.MyProgressBar_mypb_padding, padding);
//回收ta
ta.recycle();
}
当系统调用构造方法的时候,我们将布局文件中设置的属性一个个读取出来,如果用户设置了该值,那么直接读取出来使用,如果用户没有设置该值,那么我们也给了一个默认的值(默认值就是我们一开始预定义的值),OK,我们再来看看显示效果:
OK,就是这么简单。
源码下载:http://download.csdn.net/detail/u012702547/9507728
以上。