对于自定义View,一开始用的都是别人的,当时刚学Android的时候,想到自定义View,觉得是挺难的,也比较抵触,不愿意去看,到后来觉得,不可能一直用别人的,得学会自己开发,然后,就试着学了。我学习的历程,首先肯定是在网上查阅资料,看博客,看视频,跟着敲代码,然后,就自己试着写了。
下面来写一个比较简单易懂的例子,实现一个类似TextView的控件:
自定义属性:
为一个View提供可自定义的属性非常简单,只需要在res资源目录的values目录下创建一个attr.xml属性定义文件,并在该文件中通过如下定义相应的属性即可。
<declare-styleable name="MyTextView">
<attr name="title_text" format="string" />
<attr name="title_color" format="color" />
<attr name="title_size" format="dimension" />
<attr name="background_color" format="color" />
</declare-styleable>
在自定义属性的时候,我一开始犯了一个错误,这里得注意一下,在attr标签中定义的属性名不能是已经存在的,不然会报找不到这个属性的错误,这里一定要谨记。
- 继承View,实现其构造方法:
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
obtainStyledAttrs(context, attrs); //初始化,获取自定义属性
}
这儿有一个小技巧,在写构造方法的时候,一个参数的使用this调用两个参数的构造方法,以此类推,最后的初始化在三个参数的构造方法中完成,这样,只需把初始化的代码放入三个参数的构造方法中,就可以统一在其他构造方法调用,这样写的好处在于比较简洁,避免在每一个构造方法中都要单独的写上初始化的代码。
还有一点需要特别注意,上面叙述中的是使用this进行构造方法的调用,千万不要用super,一定要注意!!!后面会有讲到。
获取自定义属性值:
在获取自定义属性值之前,还需要做一件事,就是为属性值设置默认值以及参数的声明,如下:
private static final int DEFAULT_TITLE_SIZE = 10;
private static final int DEFAULT_TITLE_COLOR = 0x000000;
private static final int DEFAULT_BACKGROUND_COLOR = 0xffffff;
private int titleSize = sp2px(DEFAULT_TITLE_SIZE);
private int titleColor = DEFAULT_TITLE_COLOR;
private int background_color = DEFAULT_BACKGROUND_COLOR;
private String titleText = "";
获取属性值的代码如下:
/**
- 获取自定义属性
- * @param attrs
*/
private void obtainStyledAttrs(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
titleSize = (int) array.getDimension(R.styleable.MyTextView_title_size, titleSize);
titleColor = array.getColor(R.styleable.MyTextView_title_color, titleColor);
titleText = array.getString(R.styleable.MyTextView_title_text);
background_color = array.getColor(R.styleable.MyTextView_background_color, background_color);
array.recycle();
}
接下来是View的测量,相信大家都知道,在自定义View中需要完成几件事,分别是onMeasure、onDraw(尤为重要),下面,进入onMeasure,也就是测量View。
那么问题来了,如何测量呢?在Android中方为我们提供了一个设计短小精悍却功能强大的类——MeasureSpec类,通过它来帮助我们测量View。MeasureSpec是一个32位的int值,其中高2位为测量的模式,低30位为测量的大小,在计算中使用位运算的原因是为了提高并优化效率。
测量模式分为三种:EXACTLY:即精确模式,当我们将控件的layout_width属性或layout_height属性指定为具体数值时,比如android:layout_width=”100dp”,或者指定为match_parent时(占据父View的大小),系统使用的是EXACTLY模式。
AL_MOST:即最大模式,当控件的layout_width属性或layout_height属性指定为wrap_content时,控件大小一般随着控件的子空间或内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。
UPSPECIFIED:这个属性比较奇怪——它不指定其大小测量模式,View想多大就多大。
简单的介绍完了MeasureSpec的三种测量模式之后,下面上代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureDimension(200, widthMeasureSpec), measureDimension(100, heightMeasureSpec));
}
public int measureDimension(int defaultSize, int measureSpec) {
int result;
//获取测量模式
int specMode = MeasureSpec.getMode(measureSpec);
//获取值
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = defaultSize; //UNSPECIFIED
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
- 最后一步便是onDraw,代码如下:
@Override
protected void onDraw(Canvas canvas) {
canvas.save(); //保存绘制状态
Paint paint = new Paint(); //画笔
paint.setColor(background_color); //设置绘制的颜色
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);
canvas.save(); //此处绘制矩形
paint.setColor(titleColor);
paint.setTextSize(titleSize);
Rect rect = new Rect();
//将文本内容放入Rect中,以便获取文本的宽高
paint.getTextBounds(titleText, 0, titleText.length(), rect);
canvas.drawText(titleText, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint); //绘制文本
canvas.restore();
}
到此,自定义View已完成,此处需特别注意一个问题,在写构造函数的时候,我一开始没注意,使用了系统默认的super,后来把代码基本写完之后,发现使用的自定义View没有任何效果,屏幕上一片空白,之后发现了问题所在,因为使用super调用的是父类的方法,要想调用本类的构造方法,必须改为this,切记。
完整的代码如下:
public class MyTextView extends View {
private static final int DEFAULT_TITLE_SIZE = 10;
private static final int DEFAULT_TITLE_COLOR = 0x000000;
private static final int DEFAULT_BACKGROUND_COLOR = 0xffffff;
private int titleSize = sp2px(DEFAULT_TITLE_SIZE);
private int titleColor = DEFAULT_TITLE_COLOR;
private int background_color = DEFAULT_BACKGROUND_COLOR;
private String titleText = "";
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
obtainStyledAttrs(context, attrs);
}
/**
* 获取自定义属性
*
* @param attrs
*/
private void obtainStyledAttrs(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
titleSize = (int) array.getDimension(R.styleable.MyTextView_title_size, titleSize);
titleColor = array.getColor(R.styleable.MyTextView_title_color, titleColor);
titleText = array.getString(R.styleable.MyTextView_title_text);
background_color = array.getColor(R.styleable.MyTextView_background_color, background_color);
array.recycle();
}
public int getTitleSize() {
return titleSize;
}
public int getTitleColor() {
return titleColor;
}
public String getTitleText() {
return titleText;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureDimension(200, widthMeasureSpec), measureDimension(100, heightMeasureSpec));
}
public int measureDimension(int defaultSize, int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = defaultSize; //UNSPECIFIED
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
//save方法是将当前的结果推到栈里,相当于保存多个历史记录
canvas.save();
Paint paint = new Paint();
paint.setColor(background_color);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);
canvas.save();
paint.setColor(titleColor);
paint.setTextSize(titleSize);
Rect rect = new Rect();
//getTextBounds方法一定要在setColor...之后
//此方法是将text文本放入已新建的Rect容器中,由此得到文本内容的宽高
paint.getTextBounds(titleText, 0, titleText.length(), rect);
//此处不能用getMeasuredWidth()替换getWidth()
canvas.drawText(titleText, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint);
//restore方法是将最后一个结果弹出栈,相当于只留下最后一个修改的状态作为最终结果,而之前的状态全部被删除
canvas.restore();
}
/**
* dp转换为px
*
* @param dpVal
* @return
*/
protected int dp2px(int dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, getResources().getDisplayMetrics());
}
/**
* sp转换为px
*
* @param spVal
* @return
*/
protected int sp2px(int spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, getResources().getDisplayMetrics());
}
}