在前面几篇博客了解了Android中View的绘制流程和自定义View的几个常用类,在这一篇博客中主要介绍一下Android中自定义View的基本步骤以及简单的使用自定义属性。
通常来说,自定义组件有两种定义方式:
从0开始定义自定义组件,组件类继承View;
从Android已有的组件进行扩展,定义出更加个性化或者更适合需求的控件。
Android自定义View的基本步骤:
① 对于直接继承自View的组件:
创建继承自View的自定义组件,第一步就是重写构造方法,接着就是重写onMeasure()方法进行测量和onDraw()方法绘制结果,第三步就是处理相关的事件,在这里主要说第一、二步,事件的处理可以浏览《Android中的事件分发机制》这篇博客。
以下代码我创建了一个继承自View的自定义组件,并重写了onMeasure()方法和onDraw()方法:
public class TestView extends View {
public TestView(Context context) {
super(context);
}
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量组件大小
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
// 绘制组件
super.onDraw(canvas);
}
}
四个构造方法
第一个只有一个参数,是在代码中创建组件时调用,比如创建一个按钮:Button btn = new Button(this),this是Context的子类;
第二个构造方法在layout布局文件中使用组件时调用,参数attrs表示当前配组件在xml文件中的属性集合,例如在要layout.xml中定义一个按钮:,Android会调用第二个构造方法创建出Button对象;
第三个构造方法是不会自动调用的,当我们在Theme中定义了Style属性时通常在第二个构造方法中手动调用;
第四个构造方法是在Android5.0以上才加上的。
测量大小
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec)
该方法主要用于子类的重写和扩展,如果不重写该方法,父类View有自己的默认实现。在Android中,自定义组件的大小都由自身通过onMeasure()进行测量,不管界面布局有多么复杂,每个组件都负责计算自己的大小。
绘图
protected void onDraw(Canvas canvas)
该方法用于显示组件的外观,界面最终的显示结果就是通过canvas绘制出来的结果。在View类中,该方法并没有任何的默认实现。
需要了解更多关于onMeasure()方法和onDraw()方法的可以查看《Android自定义View之View的绘制流程》这篇博客。
事件处理
如果需要事件处理,就重写dispatchTouchEvent(MotionEvent event)和onTouchEvent(MotionEvent event)方法。
需要了解更多关于事件处理相关的内容,可以查看《Android中的事件分发机制》这篇博客。
例:下面的代码继承自View,并且重写了onMeasure()和onDraw()方法,在界面上绘制了一段文字:
public class TestView extends View {
private String text = "Android自定义组件练习";
private Paint paint;
public TestView(Context context) {
this(context, null); // 调用两个参数的构造方法
}
public TestView(Context context, AttributeSet attrs) {
this(context, attrs, 0);// 调用三个参数的构造方法
}
public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();//初始化
}
/**初始化方法*/
private void init() {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.parseColor("#f80000"));
paint.setTextSize(50);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();//初始化
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量组件大小
int width = getViewWidth(widthMeasureSpec);//获取控件的宽
int height = getViewHeight(heightMeasureSpec);//获取控件的高
setMeasuredDimension(width,height);//设置控件的宽和高为测量得到的宽和高
}
/**获取控件的高*/
private int getViewHeight(int heightMeasureSpec) {
int mode = MeasureSpec.getMode(heightMeasureSpec); // 获取高度模式
int height = MeasureSpec.getSize(heightMeasureSpec); // 获取高度值
if (MeasureSpec.EXACTLY == mode) {
// 如果模式是精确的,直接返回高度值
return height;
}else{
// 如果模式不是精确的,就进行测量
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds); // 测量文字的高度
return bounds.height() + 20; // 文字的高度加上20像素作为边距作为控件的高度
}
}
/**获取控件的宽*/
private int getViewWidth(int widthMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec); // 获取宽度模式
int height = MeasureSpec.getSize(widthMeasureSpec); // 获取宽度值
if (MeasureSpec.EXACTLY == mode) {
// 如果模式是精确的,直接返回高度值
return height;
}else{
// 如果模式不是精确的,就进行测量
Rect bounds = new Rect();
paint.getTextBounds(text,0,text.length(),bounds); // 测量文字的宽度
return bounds.width() + 20; // 文字的宽度加上20像素作为边距作为控件的宽度
}
}
@Override
protected void onDraw(Canvas canvas) {
// 绘制组件
canvas.drawText(text,10,getMeasuredHeight() - 10,paint);// 直接将文字绘制到界面上
}
}
在layout.xml中直接使用运行得到结果如图:
② 对于直接继承自ViewGroup的组件:
创建继承自ViewGroup的自定义组件,主要步骤和继承自View的相差不大。第一步就是重写构造方法,接着就是重写onMeasure()方法进行测量,只是在这里需要递归测量所有子组件的大小;不需要重写onDraw()方法,可以重写dispatchDraw()方法调用子组件的绘制方法让子组件绘制自身,另外不同的就是继承至ViewGroup的自定义组件必须重写onLayout()方法给每一个孩子控件确定位置(当我们继承FrameLayout和LinearLayout类似的容器组件时,没有重写onLayout()方法是因为这些容器组件内已经实现onLayout()方法),第三步就是处理相关的事件,在这里主要说第一、二步,事件的处理可以查看《Android中的事件分发机制》这篇博客
了解ViewGroup中的dispatchDraw()方法调用View中的onDraw()方法过程和ViewGroup的onMeasure()方法简单用例,可以查看《Android自定义View之View的绘制流程》这篇博客。
在自定义控件中使用自定义属性:
在TestView类中,我们并没有做特别定义就能使用一些属性,那是因为组件从View继承后,View已经具备了一些属性,比如layout_width、layout_height,所以不需要定义就能使用。在Android\sdk\platforms\android-23\data\res\values\attrs.xml文件中,找到
但是现在我想在xml文件中直接可以指定文字的大小、颜色以及绘制的文字内容,在默认的View中就没有这样的属性,也就是需要我们自定义,具体的定义过程如下:
Android中自定义属性主要有以下几个步骤:
◆ 在 res/values/attrs.xml 文件中为指定组件定义 declare-styleable 标记, 并将所有的属性都定义在该标记中;
◆ 在 layout 文件中使用自定义属性;
◆ 在组件类的构造方法中读取属性值。
现在就以TestView为例,实现在xml文件中可以改变字体大小、颜色和文字内容:
第1步,在res/values/attrs.xml文件的resources节点下定义如下内容:
第2步,在xml文件中使用刚刚定义的属性:
注意,xml文件在使用自定义的属性时,前面并不是使用的android:开头,表示这不是Android中的属性,所以我们添加自定义的名称空间:
xmlns:renj="http://schemas.android.com/apk/res-auto"
完整的layout.xml文件:
第3步,在组件类中读取自定义属性:
使用上面的代码,修改的部分如下,就可以读取到自定的属性了
public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);//初始化
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context,attrs);//初始化
}
/**初始化方法*/
private void init(Context context,AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.TestView);// 获取属性数组
text = typedArray.getString(R.styleable.TestView_text);// 获取绘制内容,注意使用的方法getString()
color = typedArray.getColor(R.styleable.TestView_textColor, DEFAULT_COLOR); //获取颜色,没有定义使用默认值
textSize = typedArray.getDimension(R.styleable.TestView_textSize, DEFAULT_SIZE); //获取文字大小,没有定义使用默认值
typedArray.recycle(); // 释放资源
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setTextSize(textSize);
}
注意:获取属性时,使用typedArray调用获取属性的方法时,如果是string类型的就调用getString(),如果获取颜色就调用getColor(),以此类推。
运行结果如图:组件的属性都应该定义在declare-styleable标记中,该标记的name属性值一般来说都是组件类的名称(此处为TestView),虽然也可以取别的名称,但和组件名相同可以ᨀ高代码的可读性。组件的属性都定义在declare-styleable标记内,成为declare-styleable标记的子标记,每个属性由两部分组成:属性名和属性类型。属性通过attr来标识,属性名为name,属性类型为format,可选的属性类型如图:
说明:
string:字符串
boolean:布尔
color:颜色
float:浮点数
fraction类型的属性
integer:整数
fraction:百分数,在动画资源
dimension:尺寸,可以带单位,比如长度通常为dp,字体大小通常为sp
enum:枚举,需要在attr标记中使用
flag类型的属性也有一个子标记
组件中常见的gravity属性就是属性flag类型,如图: