最近在学习View的绘制流程,这里就把学习到的内容做个记录吧。
首先,View的绘制基本上是测量、布局和绘制三个步骤。而View对应这些步骤有measure()、layout()和draw()三个方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
“测量视图及其内容,以确定测量的宽度和测量的高度。”,注意这个方法是final的,不可以重写。
public void layout(int l, int t, int r, int b)
“分配一个视图和它的所有子View的大小和位置。”
public void draw(Canvas canvas)
“手动将此视图(和所有的子视图)渲染给给定的画布。”,注意视图必须在这个函数被调用之前已经做了一个完整的布局,也就是在layout完成后才可以调用。
新建一个继承ImageView的自定义View,代码如下:
public class MyImageView extends ImageView{
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d("MyImageView", "onMeasure");
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
Log.d("MyImageView", "OnLayout");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d("MyImageView", "onDraw");
}
}
再布局文件中使用这个自定义ImageView,运行,可以看到Log:
这里可以看到视图绘制过程中,三个方法的执行顺序是:
onMeasure() → onLayout() → onDraw()
那么也按照这个顺序一个个方法看下去。
measure()
直接先上源码:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
这就是为什么重写onMeasure()的原因,因为measure()不可重写,但实际上它的测量在onMeasure()中完成。
来看一看measure()的两个参数,为什么不直接叫width和height,其实是因为这两个int值包含了MeasureSpec对象的信息,MeasureSpec对象由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。
int值的高2位表示MODE,MODE定义在MeasureSpec中,有三种类型:
① UNSPECIFIED 父视图没有做任何约束,视图可以是希望的任何大小;
② AT_MOST 视图可以是设定的任意大小,但最大值受到specMode的限制;
③ EXACTLY 视图是给定确定的大小。
而specMode呢,是int值的低30位。
那么它们从哪里来,在哪里构造MeasureSpec对象呢?,查看源码发现:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
其中lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。
private int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
在MATCH_PARENT和WRAP_CONTENT的时候,spceSize就是windowSize,所以这就是根布局是全屏的原因。UNSPECIFIED在什么情况下触发呢?这个很少,但还是有的,比如scrollView控件。
measure()够清晰了,让我们直接去看View的onMeasure()。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
可以看到其实里面调用了setMeasuredDimension()这个方法,这就是为什么在重写onMeasure()时,要么调用超类的onMeasure(),要么调用setMeasuredDimension()的原因。而这之后就能通过getMeasureWidth()和getMeasureHeight()获得measureWidth和measureHeight了。
setMeasuredDimension()默认会调用getDefaultSize()方法来获取视图的宽高。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
第一个参数是Android建议的size,通过getSuggestedMinimumWidth()和getSuggestedMinimumHeight()来获取,这个不深究,看注释可以知道是返回View应该使用的最小宽高,也就是View的默认大小,都是由View的Background尺寸与通过设置View的miniXXX属性共同决定的。
第二个参数就是上面说到可以构建MeasureSpec对象的int值。看上面的代码可以知道,如果specMode等于AT_MOST或EXACTLY就返回specSize,这就是系统默认的规格。
简单来说View的测量过程就是measure()调用onMeasure(),onMeasure()调用setMeasuredDimension()。
我们知道了View的测量的调用过程,默认值,赋值等,但只是View的测量,那么能进行嵌套的ViewGroup呢?ViewGroup里面包含一个或者多个子View,每个子View需要measure,ViewGroup是怎么做到的?
ViewGroup中定义了一个measureChildren()方法来去测量子View的大小,如下所示:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
这里首先会去遍历当前布局下的所有子View,然后逐个调用measureChild()方法来测量相应子视图的大小,看一下measureChild():
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看到,先调用getChildMeasureSpec()去计算子View的MeasureSpec,计算的依据是父View的MeasureSpec,子View的padding值等等。然后调用子view的measure()方法,并把计算出的MeasureSpec传递进去,measure()就是上面说到的过程了。
measure完成之后,第二步就是layout,接下来看View的layout()。
layout()
也是先看一下源码:
public void layout(int l, int t, int r, int b) {
...
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
...
}
}
}
...
}
可以看到首先调用setFrame()方法来判断视图的大小是否发生过变化判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout。
如果需要layout,调用的其实是onLayout(),所以当我们需要自定义布局的时候,重写的就是onLayout(),看一下onLayout()的源码:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
这是一个空方法!重载onLayout的目的就是安排其children在父View的具体位置,所以看一下父View,通常在布局中都是ViewGroup包含着View,所以看一下ViewGroup的onLayout()。
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
是一个抽象方法~意味着ViewGroup的子类都必须重写这个方法。
重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。
那么按照这个思路,继承ViewGroup去自定义一个LinearLayout,代码如下:
public class MyLinearLayout extends LinearLayout {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
那么很简单这里就是把MyLinearLayout里面包含的第一个子View按子View本身的宽高进行在MyLinearLayout里面布局。
布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<core.flexible.activity.recyclerview.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<core.flexible.activity.recyclerview.MyImageView
android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />
</core.flexible.activity.recyclerview.MyLinearLayout>
运行截图:
那么看一下绘制逻辑,首先是像上面measure过程一样,onMeasure()方法会在onLayout()方法之前调用,所以现在在onMeasure()方法中判断LinearLayout中是否有包含一个子View,有则调用measureChild()测量出子View的大小。
然后在MyLinearLayout的onLayout()中判断是否有包含子View,然后调用子View的layout()方法来确定它在MyLinearLayout布局中的位置,传入参数分别代表着子View在MyLinearLayout中左上右下四个点的坐标,然后layout完成,draw(具体过程后面draw()里面再说)。
那么如果想改变子View的位置只需要改变这四个坐标就可以了。
这里要说一下,在onLayout()执行之后,我们可以通过调用getWidth()方法和getHeight()方法来获取视图的宽高。这里又有一个宽高!
上面说到onMeasure()(实际上只要setMeasuredDimension()被调用之后)就可以通过getMeasureWidth()和getMeasureHeight()获得measureWidth和measureHeight。
那么两者的区别是什么呢?查找源码:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
可以清楚发现,getMeasuredWidth()和getMeasuredHeight()的值是measure时算好的宽高,getWidth()和getHeight()的宽高是layout(l, t, r, b)里面参数的计算后的值,所以onMeasure()之后得到的宽高值有可能和onLayout()之后得到的宽高不一样。
接下来就到了绘制的最后一步,draw()
draw()
源码:
public void draw(Canvas canvas) {
...
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
}
代码很长,看注释明显分为了6步,其中step2和step5可以跳过不管,来看其它。
step1:对背景进行绘制。
step3:对视图的内容进行绘制。可以看到,这里调用了onDraw(),进去一看发现,又是个空方法!。因为视图的内容不一定一样,所以具体的绘制实现就交由视图自己实现。
step4:当前视图如果存在子View,对所有子View进行绘制。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。
step6:,对视图的滚动条进行绘制。View都有(水平垂直)的滚动条,一般不显示。
那么整个draw过程也很清晰了,也明白了为什么是重写onDraw()。
整个流程就先到这~