问:自定义View有几个构造函数,及自定义View的主要流程
答:自定义View中共有四个构造函数,一般只需要实现一个参数及两个参数的构造函数即可。自定义View过程中,主要流程有:measure、layout、draw即 测量、布局、绘制,这里面涉及到MeasureSpec、Paint、Canvas、Path等很多重要类。
自定义View的实现方式有很多:自定义组合控件、继承系统View 如继承TextView、继承系统ViewGroup 如继承LinearLayout、继承View、继承ViewGroup等。
自定义View的四个构造方法:
class MyView : View {
/**
* 在java代码里new的时候会用到
*/
constructor(context: Context?) : super(context) {}
/**
* 在xml布局文件中使用时自动调用
*/
constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {}
/**
* 不会自动调用,如果有默认style时,在第二个构造函数中调用
*/
constructor(context: Context?, @Nullable attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
/**
* 只有在API版本>21时才会用到
* 不会自动调用,如果有默认style时,在第二个构造函数中调用
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context?, @Nullable attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
}
}
自定义View的三个主要流程:measure、layout、draw
1、Measure测量流程,从View的measure方法为入口,该方法只是做了一些初始化,之后调用onMeasure方法。来看onMeasure()方法:
//View的onMeasure源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
首先看到的是onMeasure()方法要求传入两个int类型的参数,分别是宽高。这里需要了解一下 MeasureSpec 是什么东西。
MeasureSpec是View的内部类,值保存在一个int值当中,一个int有32位,前两位是 mode(模式),后30位是 size(大小) 即:MeasureSpec = mode + size
其中mode的值有三种,UNSPECIFIED,EXACTLY、AT_MOST,
模式 | 意义 | 对应 |
---|---|---|
EXACTLY | 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size | match_parent |
AT_MOST | 最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值 | wrap_content |
UNSPECIFIED | 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大 | 不怎么用 |
在ViewGroup中的 MeasureSpec测量源码如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取测量模式
int specMode = MeasureSpec.getMode(spec);
//获取测量大小
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 当父View要求一个精确值时,为子View赋值
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//当子View是match_parent,将父View的大小赋值给子View
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 如果子View是wrap_content,设置子View的最大尺寸为父View
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局给子View一个最大界限
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 当子View是match_parent,父View的尺寸为子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 如果子View是wrap_content,父View的尺寸为子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局对子View没有做任何限制
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//通过Mode 和 Size 生成新的SpecMode 返回
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
注:这里只是给子View设置了MeasureSpec参数,真正的大小是在View中具体设置的,只是给子View做了一个限制。子View的测量模式由自身的LayoutParams和父View的MeasureSpec来决定。
回过头来看View.onMeasure()方法:就一行代码,但是有三个函数:
setMeasuredDimension(int measuredWidth, int measuredHeight) :该方法用来设置View的宽高
getDefaultSize(int size, int measureSpec): 该方法用来获取View默认的宽高
getSuggestedMinimumWidth(): 该方法用于获取Android:minWidth属性的值,如果没有则为0(如果有背景还需要判断背景与mMinWidth的大小,取大值)
这里面其实最重要的是getDefaultSize方法,对于AT_MOST和EXACTLY在View当中的处理是完全相同的,在我们自定义View时要对这两种模式做出处理。
/**
* 有两个参数size和measureSpec
* 1、size表示View的默认大小,它的值是通过`getSuggestedMinimumWidth()方法来获取的,之后我们再分析。
* 2、measureSpec则是我们之前分析的MeasureSpec,里面存储了View的测量值以及测量模式
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//从这里我们看出,对于AT_MOST和EXACTLY在View当中的处理是完全相同的。所以在我们自定义View时要对这两种模式做出处理。
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
ViewGroup除了测量自身,还需要测量子View的大小,ViewGroup中提供了对子View的测量方法:measureChildren(),在measureChildren中遍历所有子View,调用measureChild(),在measureChild中调用了View的measure()方法,让子View测量自身大小。
2、layout布局流程,layout()过程,对于View来说用来计算View的位置参数,对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。其实layout最重要的在自定义ViewGroup时的重写,对其子类进行布局。
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//onLayout方法是一个空实现,我们在自定义View的时候关注重新这个onLayout方法即可,可以看看LinearLayout的onLayout方法
onLayout(changed, l, t, r, b);
//省略很多代码....
}
}
3、draw绘制流程:draw绘制流程就是绘制View的过程,整个过程可以分为6个步骤:
1.如果需要,绘制背景
2.如果有必要,保存当前canvas
3.绘制View的内容
4.绘制子View
5.如果有必要,绘制边缘,阴影等
6.绘制装饰,如滚动条等
public void draw(Canvas canvas) {
int saveCount;
// 1. 如果需要,绘制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2. 如果有必要,保存当前canvas。
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 3. 绘制View的内容。
if (!dirtyOpaque) onDraw(canvas);
// 4. 绘制子View。
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// 如果有必要,绘制边缘,阴影等
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 6. 绘制装饰,如滚动条等等。
onDrawForeground(canvas);
// we're done...
return;
}
}
/**
* 1.绘制View背景
*/
private void drawBackground(Canvas canvas) {
//获取背景
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
//获取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,则会在平移后的canvas上面绘制背景。
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
/**
* 3.绘制View的内容,该方法是一个空的实现,在各个业务当中自行处理。
*/
protected void onDraw(Canvas canvas) {
}
/**
* 4. 绘制子View。该方法在View当中是一个空的实现,在各个业务当中自行处理。
* 在ViewGroup当中对dispatchDraw方法做了实现,主要是遍历子View,并调用子类的draw方法,一般我们不需要自己重写该方法。
*/
protected void dispatchDraw(Canvas canvas) {
}
4、Paint、Canvas、Path等相关内容可以看看: 扔物线的自定义View