自定义View从实现到原理(一)
自定义View在我自学Android开发中一直感觉是高手才能掌握的知识,因为情况太多而且界面看起来有很复杂炫酷。但是自定义View同样遵循着某些规则,这篇博客我就从这个规则入手,先实现View,在涉及原理。
自定义View实现的步骤
- 自定义View从实现到原理(一)
- 自定义View的属性
- 在View构造方法中获得我们自定义的属性
- 重写onMeasure()方法,对View的尺寸大小进行修改
- 重写onDraw()方法,进行想要的样式绘制
自定义View的属性
Android系统的空间是以android:*样式开头的(android:layout_width等),这些都是系统自带的属性,那么我们自定义的View,也同样应该拥有自己的属性。那么首先,我们在res/value目录下新建attrs.xml文件,用于定义我们的属性:
<?xml version="1.0" encoding="utf-8"?>
<!--自定义TestView的attrs属性文件-->
<resources>
<!--设置styleable的name,在构造函数中会调用-->
<declare-styleable name="TestView">
<!--自定义View的属性-->
<attr name="test_color" format="color"/>
<!--format是定义的属性的类型,类似的还有boolean等-->
</declare-styleable>
</resources>
为了方便起见我们在这里只是定义了颜色。
在View构造方法中获得我们自定义的属性
新建TestView继承于View,我们可以看到TestView有四个构造函数,各个构造函数的适用情形我会在代码块中进行注释解释:
/**
* View和ViewGroup可以利用代码直接new对象时,这时候会调用第一个构造函数,
* 生成对象后利用内部提供的属性设置方法进行属性设置
*
* @param context 传入的当前位置
*/
public TestView(Context context) {
super(context);
}
/**
* 用于加载xml布局文件时调用,通常自定义控件的自定义属性的读取需要用到这个构造函数
*
* @param context 传入当前的位置
* @param attrs styleable传入位置
*/
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 一般系统不会主动调用,需要手动调用,
* 基本上是用不到的,以后我用到了再写吧
*
* @param context 芜湖
* @param attrs 芜湖
* @param defStyleAttr 这个位置是需要手动传入的
*/
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 如果第三个参数为0或者没有定义defStyleAttr时,第四个参数才起作用,
* 我更用不到了,就不浪费时间了
*
* @param context 继续芜湖
* @param attrs 芜湖
* @param defStyleAttr 芜湖
* @param defStyleRes 一个style的资源引用
*/
public TestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
super(context, attrs, defStyleAttr, defStyleRes);
}
介绍完了构造函数这一部分,根据上面介绍的各个构造函数的用法,我们选择第二种,那么就要在xml中声明这一个自定义的View:
<com.example.day1.View.TestView
xmlns:testView="http://schemas.android.com/apk/res-auto"
android:id="@+id/testView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_below="@id/testImage"
android:layout_marginTop="20dp"
android:layout_centerHorizontal="true"
testView:test_color="@android:color/holo_blue_bright" />
这个xmlns:testView="http://schemas.android.com/apk/res-auto"是必备的,只有这样才能调用我们之前自定义的View属性attrs.xml中的test_color。
在自定义的View-TestView中获取attrs中的属性:
/**
* 定义颜色
*/
private int testColor = Color.GREEN;
/**
* 定义画笔
*/
private Paint testPaint = new Paint();
/**
* View和ViewGroup可以利用代码直接new对象时,这时候会调用第一个构造函数,
* 生成对象后利用内部提供的属性设置方法进行属性设置
*
* @param context 传入的当前位置
*/
public TestView(Context context) {
this(context, null);
}
/**
* 用于加载xml布局文件时调用,通常自定义控件的自定义属性的读取需要用到这个构造函数
*
* @param context 传入当前的位置
* @param attrs styleable传入位置
*/
public TestView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
//获取我们自定义的样式属性
TypedArray testTypeArray = context.obtainStyledAttributes(attrs, R.styleable.TestView);
//提取样式属性中的testcolor属性,如果没有,则设置卫Color.RED红色
testColor = testTypeArray.getColor(R.styleable.TestView_test_color, Color.RED);
//获取资源后及时收回
testTypeArray.recycle();
testPaint.setColor(testColor);
}
如上代码所示获取相应的属性,在获取之后一定要记得回收TypeArray变量。
重写onMeasure()方法,对View的尺寸大小进行修改
在重写onMeasure()方法之前,我们首先要了解一下MeasureSpec的specmode属性,一共有三种类型:
specmode属性 | 含义 |
AT_MOST | 最大模式,表示子布局限制在一个最大值内,一般是wrap_content类型 |
EXACTLY | 一般是设置了明确的值或者是match_parent类型 |
UNSPECIFIED | 表示子布局想多大就多大,没有限制,一般用于系统内部的测量 |
现在看这三个属性是不是有点迷茫,我一开始看的时候根本不知道这是个什么东西,我们回头来看之前写过的xml布局文件:
<com.example.day1.View.TestView
****
android:layout_width="200dp"
android:layout_height="200dp"
****/>
之前我们在布局文件中写的是定死大小的TestView,当我们把他设置为wrap_content时,就会铺满整个屏幕,和设置为match_parent的效果是一样的了,这就不符合我们的预期了,而在实际开发过程中,如果布局文件中设置为wrap_content时,我们一般都会在自定义View的代码中对宽高进行处理赋值:
/**
* 自定义View测量以及设置宽高
*
* @param widthMeasureSpec 传入的View宽度信息
* @param heightMeasureSpec 传入的View高度信息
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取布局文件的宽度模式(wrap_content等)
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
//获取布局文件的高度模式
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
//获取布局文件的宽度尺寸--返回的是像素大小
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
//获取布局文件的高度尺寸
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//判断布局文件宽高的模式是否为wrap_content
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
//如果是的话则将宽高设置为150px
setMeasuredDimension(150, 150);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
//如果只有宽度为wrap_content的话,高度设置为xml设好的,宽度为150px
setMeasuredDimension(150, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
//同上
setMeasuredDimension(widthSpecSize, 150);
}
}
这个时候我们将xml改为:
<com.example.day1.View.TestView
****
android:layout_width="wrap_content"
android:layout_height="wrap_content"
****/>
效果并不是铺满全屏了,而是150px的大小。
重写onDraw()方法,进行想要的样式绘制
自定义View的onDraw()方法,负责的就是画出自己想要的View的具体模样,刚开始是这样:
/**
* 绘制View部份
*
* @param canvas 画板
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
这个canvas,就像上面注释写的那样,本质上来说是个画板,我们的View绘制就是在canvas的基础上来进行的,我们这次知识介绍最简单的部份:
canvas的方法有很多,可以直接调用它来进行绘制正方形(drawRect方法),绘制bitmap(drawBitmap方法)等等,这一部分我们后面再看,接下来就简单的看看绘制正方形:
/**
* Draw the specified Rect using the specified paint. The rectangle will be filled or framed
* based on the Style in the paint.
*
* @param left The left side of the rectangle to be drawn
* @param top The top side of the rectangle to be drawn
* @param right The right side of the rectangle to be drawn
* @param bottom The bottom side of the rectangle to be drawn
* @param paint The paint used to draw the rect
*/
public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) {
super.drawRect(left, top, right, bottom, paint);
}
这就是绘制正方形drawRect的源码,从这里面我们可以看到,一共需要五个参数,左上右下以及画笔,看完了源码我们直接应用:
canvas.drawRect(0, 0, getWidth(), getHeight(), testPaint);
很简单的一行代码,就可以绘制出一个正方形,接下来贴出全部的代码以及最终的样式:
/**
* 自定义的TestView
*/
public class TestView extends View {
/**
* 定义颜色
*/
private int testColor = Color.GREEN;
/**
* 定义画笔
*/
private Paint testPaint = new Paint();
/**
* View和ViewGroup可以利用代码直接new对象时,这时候会调用第一个构造函数,
* 生成对象后利用内部提供的属性设置方法进行属性设置
*
* @param context 传入的当前位置
*/
public TestView(Context context) {
this(context, null);
}
/**
* 用于加载xml布局文件时调用,通常自定义控件的自定义属性的读取需要用到这个构造函数
*
* @param context 传入当前的位置
* @param attrs styleable传入位置
*/
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//获取我们自定义的样式属性
TypedArray testTypeArray = context.obtainStyledAttributes(attrs, R.styleable.TestView);
//提取样式属性中的testcolor属性,如果没有,则设置卫Color.RED红色
testColor = testTypeArray.getColor(R.styleable.TestView_test_color, Color.RED);
//获取资源后及时收回
testTypeArray.recycle();
testPaint.setColor(testColor);
}
/**
* 一般系统不会主动调用,需要手动调用,
* 我现在基本上是用不到的,以后我用到了再写吧
*
* @param context 芜湖
* @param attrs 芜湖
* @param defStyleAttr 这个位置是需要手动传入的
*/
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 如果第三个参数为0或者没有定义defStyleAttr时,第四个参数才起作用,
* 我更用不到了,就不浪费时间了
*
* @param context 继续芜湖
* @param attrs 芜湖
* @param defStyleAttr 芜湖
* @param defStyleRes 一个style的资源引用
*/
public TestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* 自定义View测量以及设置宽高
*
* @param widthMeasureSpec 传入的View宽度信息
* @param heightMeasureSpec 传入的View高度信息
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取布局文件的宽度模式(wrap_content等)
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
//获取布局文件的高度模式
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
//获取布局文件的宽度尺寸--返回的是像素大小
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
//获取布局文件的高度尺寸
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//判断布局文件宽高的模式是否为wrap_content
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
//如果是的话则将宽高设置为150px
setMeasuredDimension(150, 150);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
//如果只有宽度为wrap_content的话,高度设置为xml设好的,宽度为150px
setMeasuredDimension(150, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
//同上
setMeasuredDimension(widthSpecSize, 150);
}
}
/**
* 绘制View部份
*
* @param canvas 画板
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0, 0, getWidth(), getHeight(), testPaint);
}
}
只是个很简单的例子,记录我学习的进展,下一篇会介绍View的分发机制以及View的工作流程等,从View的实现入手,真正掌握所有关于自定义View的知识。