自定义View是Android开发中不可避开的一个重点,也是难点。一方面自定义View涉及到的的知识点较多,从基础的坐标、Paint和Canvas的使用到Drawable、动画,更复杂的可能还会涉及Shader以及混合模式Xfermode等等(当然可能不止如此);另一方面自定义View的过程比较复杂,要了解View的测量、布局、绘制流程,重写相应的方法。自己一直有个想法,想把自己在学习或工作过程中所学到的关于自定义View的知识点梳理一下,记录下来,让自己对自定义View的完整流程有一个清晰的认识。
就像罗马不是一天建成的一样,再复杂的UI也是由一个一个基础的方法绘制而成的,因此熟练掌握基本功是非常重要的,另外,不要急着写代码,要先学会将一个复杂的过程分解,解耦成一个个子过程,形成一个大致的思路,然后再开始敲代码。
下面举一个例子,假设要实现下面的圆形进度条:
首先,学会将一个复杂的过程分解,拆分一下圆形进度条的绘制过程:
1、先绘制一个圆圈,作为整个圆形进度条的背景(上图采用的背景颜色是透明的所以看不出);
2、绘制外层的圆弧(白色部分),表示完整的进度;
3、绘制外层表示当前进度的圆弧(黄色部分);
4、绘制圆圈中间表示进度的文本。
这样就可以将一个看起来比较复杂的过程解耦成一个个子过程,子过程实现起来就没有那么复杂了。当然,这里举的例子可能谈不上复杂,只是想说明一下一个理念,学会将一个复杂的过程分解成一个一个简单的子过程。
在形成一个大体的思路后就可以开始动手写代码了!
通常自定义View的时候会提供一些可配置的属性让开发人员可以直接在xml布局中配置而不需要去修改View的Java类文件。以上图为例,可配置属性包括:背景色、进度条的颜色及宽度、进度条文本的字体大小及颜色、百分号的字体大小及颜色等等,完整如下:
private int mMax;//进度最大值
private int mProgress;//当前进度
private float mRingWidth;//圆环宽度
private int mRingBackgroung;//圆环背景色
private int mRingColor;//圆环颜色
private float mRingPadding;//圆环与圆圈的边距
private int mCircleColor;//圆圈背景色
private float mProgressTextSize;//进度文本字体大小
private int mProgressTextColor;//进度文本字体颜色
private String mSuffixText;//百分号文本
private float mSuffixTextSize;//百分号字体大小
private int mSuffixTextColor;//百分号字体颜色
private float mSuffixTextPadding;//百分号文本和进度文本的边距
然后在res\values目录下新建一个attrs.xml并配置相应属性名称及类型:
<declare-styleable name="CircleProgress">
<attr name="max" format="integer"/>
<attr name="progress" format="integer"/>
<attr name="ring_width" format="dimension"/>
<attr name="ring_color" format="color"/>
<attr name="ring_backgroung" format="color"/>
<attr name="ring_padding" format="dimension"/>
<attr name="circle_color" format="color"/>
<attr name="progress_textSize" format="dimension"/>
<attr name="progress_textColor" format="color"/>
<attr name="suffix_text" format="string"/>
<attr name="suffix_textSize" format="dimension"/>
<attr name="suffix_textColor" format="color"/>
<attr name="suffix_textPadding" format="dimension"/>
</declare-styleable>
这样所有要提供给外界进行配置的属性就声明完成了。
View的构造函数有四个:
public View(Context context);
public View(Context context, AttributeSet attrs);
public View(Context context, AttributeSet attrs, int defStyleAttr);
public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes);
但是一般来说,只需要覆盖前面两个构造方法就可以了,当在代码中创建View的时候使用View(Context),当从XML中引用View的时候使用View(Context,AttributeSet)。以上图为例,需要在第2个构造函数中通过Context.obtainStyledAttributes方法获取自定义属性并设置默认值,代码如下:
public CircleProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgress);
mMax = ta.getInteger(R.styleable.CircleProgress_max, 100);
mProgress = ta.getInteger(R.styleable.CircleProgress_progress, 0);
setMax(mMax);
setProgress(mProgress);
mRingWidth = ta.getDimension(R.styleable.CircleProgress_ring_width, SizeUtil.dp2px(getResources(), 15));
mRingColor = ta.getColor(R.styleable.CircleProgress_ring_color, Color.parseColor("#ffe401"));
mRingBackgroung = ta.getColor(R.styleable.CircleProgress_ring_backgroung, Color.parseColor("#eeeeee"));
mRingPadding = ta.getDimension(R.styleable.CircleProgress_ring_padding, SizeUtil.dp2px(getResources(), 0));
mCircleColor = ta.getColor(R.styleable.CircleProgress_circle_color, Color.parseColor("#ffffff"));
mProgressTextSize = ta.getDimension(R.styleable.CircleProgress_progress_textSize, SizeUtil.dp2px(getResources(), 25));
mProgressTextColor = ta.getColor(R.styleable.CircleProgress_progress_textColor, Color.parseColor("#ffe401"));
mSuffixText = ta.getString(R.styleable.CircleProgress_suffix_text);
if (TextUtils.isEmpty(mSuffixText))
mSuffixText = "%";
mSuffixTextSize = ta.getDimension(R.styleable.CircleProgress_suffix_textSize, SizeUtil.dp2px(getResources(), 15));
mSuffixTextColor = ta.getColor(R.styleable.CircleProgress_suffix_textColor, Color.parseColor("#ffe401"));
mSuffixTextPadding = ta.getDimension(R.styleable.CircleProgress_suffix_textPadding, SizeUtil.dp2px(getResources(), 2));
}
View类默认的onMeasure方法只支持EXACTLY模式,所以如果自定义控件的时候不重写onMeasure()方法,就只能使精确值模式,控件可以响应具体的宽高值和match_parent属性,不支持wrap_content,如果要让自定义View支持wrap_content属性,就必须重写onMeasure()方法指定wrap_content时的大小。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = 200;//指定宽度wrap_content时为200px
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
private int measureHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = 100;//指定高度wrap_content时为100px
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
以上图为例,已经知道了怎么去绘制圆形进度条了,那么接下来要做的就是调用Canvas的相应方法去完成就可以了,如canvas.drawCircle、canvas.drawArc等等,代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mCirclePaint.setAntiAlias(true);
mCirclePaint.setColor(mCircleColor);
canvas.drawCircle(mWidth / 2.0f, mHeight / 2.0f, mWidth / 2.0f, mCirclePaint);
mArcPaint.setAntiAlias(true);
mArcPaint.setColor(mRingBackgroung);
mArcPaint.setStyle(Paint.Style.STROKE);
mArcPaint.setStrokeWidth(mRingWidth);
canvas.drawArc(mRectf, 0, 360, false, mArcPaint);
mArcPaint.setColor(mRingColor);
mArcPaint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawArc(mRectf, -90, -(float) mProgress / getMax() * 360, false, mArcPaint);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(mProgressTextSize);
mTextPaint.setColor(mProgressTextColor);
String value = String.valueOf(getProgress());
mTextPaint.setColor(mProgressTextColor);
mTextPaint.setTextSize(mProgressTextSize);
float textHeight = mTextPaint.descent() + mTextPaint.ascent();
float textBaseline = (getHeight() - textHeight) / 2.0f;
canvas.drawText(value,
(getWidth() - mTextPaint.measureText(value)) / 2.0f,
textBaseline, mTextPaint);
mTextPaint.setTextSize(mSuffixTextSize);
mTextPaint.setColor(mSuffixTextColor);
float suffixHeight = mTextPaint.descent() + mTextPaint.ascent();
canvas.drawText(mSuffixText,
getWidth() / 2.0f + mTextPaint.measureText(value)
+ mSuffixTextPadding, textBaseline + textHeight
- suffixHeight, mTextPaint);
}
这里说一下,在XML中使用自定义属性时要创建一个新的命名空间,如在根布局可以声明:
xmlns:app="http://schemas.android.com/apk/res-auto"
<com.elestic.CircleProgress
android:id="@+id/circle_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:progress="14"
app:ring_width="20dp"
app:ring_color="#ff9920"
app:circle_color="@android:color/transparent"
app:suffix_textSize="32dp"
app:suffix_textColor="#ff9920"
app:progress_textSize="60dp"
app:progress_textColor="#ff9920" />
虽然自定义View有的很复杂,有的却很简单,但是总体来说,过程大同小异,简单的基本流程可以归纳如下:
第一、自定义布局属性,这个在res\values\attrs.xml中配置属性名称和类型;
第二、继承View,覆盖构造方法,获取并设置自定义布局属性的默认值;
第三、如果有必要,重写onMeasure方法,指定wrap_content时的大小;
第四、重写onDraw方法,绘制控件;
第五、在XML布局文件中引用自定义View,在代码中实现View的动画过程或属性的动态设置等。
Android自定义View(一)坐标系解读
Android自定义View(二)屏幕尺寸信息和单位转换
Android自定义View(三)自定义属性AttributeSet
Android自定义View(四)Paint的常用方法
Android自定义View(五)Canvas的常用方法
Android自定义View(六)插值器和估值值
Android自定义View(七)属性动画的使用
。。。。。。