1.整体流程
View的首次绘制流程从ViewRootImpl的setView()
方法开始
//ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();//检查是否在主线程
mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//post一个runnable处理-->mTraversalRunnable
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
``````
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();//View的绘制流程正式开始。
}
ViewRootImpl在其创建过程中通过requestLayout()向主线程发送了一条触发遍历操作的消息,遍历操作是指performTraversals()方法。它是一个包罗万象的方法。
ViewRootImpl中接收的各种变化,如来自WMS的窗口属性变化、来自控件树的尺寸变化及重绘请求等都引发performTraversals()的调用,并在其中完成处理。
View类及其子类中的onMeasure()、onLayout()、onDraw()等回调也都是在performTraversals()的执行过程中直接或间接的引发。也正是如此,一次次的performTraversals()调用驱动着控件树有条不紊的工作,一旦此方法无法正常执行,整个控件树都将处于僵死状态。因此performTraversals()函数可以说是ViewRootImpl的心跳。
//ViewRootImpl
private void performTraversals() {
//调用performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);
performLayout(lp, mWidth, mHeight);
performDraw();
}
首先我们看一下performTraversals()首次绘制的大致流程,performTraversals()会依次调用performMeasure、performLayout、performDraw三个方法,这三个方法便是View绘制流程的精髓所在。
performMeasure : 会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传到子元素中了,这样就完成了一次measure过程。measure完成以后,可以通过getMeasuredWidth和getMeasureHeight方法来获取到View测量后的宽高。
performLayout : 和performMeasure同理。Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成以后,可以通过getTop/Bottom/Left/Right拿到View的四个顶点位置,并可以通过getWidth和getHeight方法来拿到View的最终宽高。
performDraw : 和performMeasure同理,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的。Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。
因此可以得出View的绘制分为measure,layout, draw 三个阶段:
- measure测量 :根据父 view 传递的 MeasureSpec 进行计算大小。
- layout布局:根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。
- draw绘制 :把 View 对象绘制到屏幕上。
2.measure测量
View的测量的目的便是 测量控件的宽高值,围绕该目的,View的设计者通过代码编织了一整套复杂的逻辑:
1、对于子View而言,其本身宽高直接受限于父View的 布局要求,举例来说,父View被限制宽度为40px,子View的最大宽度同样也需受限于这个数值。因此,在测量子View之时,子View必须已知父View的布局要求,这个 布局要求, Android中通过使用 MeasureSpec 类来进行描述。
2、对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件本身未测量完毕,父控件自身的测量亦无从谈起。
Android中View的测量流程中使用了非常经典的 递归思想:对于一个完整的界面而言,每个页面都映射了一个View树,其最顶端的父控件测量开始时,会通过 遍历 将其 布局要求 传递给子控件,以开始子控件的测量,子控件在测量过程中也会通过 遍历 将其 布局要求 传递给它自己的子控件,如此往复一直到最底层的控件。
而当最底层位置的子控件自身测量完毕后,其父控件会将所有子控件的宽高数据进行聚合,然后通过对应的 测量策略 计算出父控件本身的宽高,测量完毕后,父控件的父控件也会根据其所有子控件的测量结果对自身进行测量,最终完成最顶层父控件的测量,至此界面整个View树测量完毕。
在整个 测量流程 中, 布局要求 都是一个非常重要的核心名词,Android中通过使用 MeasureSpec 类来对其进行描述。
子控件的测量过程本身还应该依赖于父控件的一些布局约束,子控件的测量结果是由父控件和其本身共同决定的 ,而父控件对子控件的布局约束,便是前文提到的 布局要求,即MeasureSpec类。
MeasureSpec
public final class MeasureSpec {
int size; // 测量大小
Mode mode; // 测量模式
enum Mode { UNSPECIFIED, EXACTLY, AT_MOST }
MeasureSpec(Mode mode, int size){
this.mode = Mode;
this.size = size;
}
public int getSize() { return size; }
public Mode getMode() { return mode; }
}
MeasureSpec分成了2个属性。
测量大小 意味着控件需要对应大小的宽高;
测量模式 则表示控件对应的宽高模式:
- UNSPECIFIED:父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小;日常开发中自定义View不考虑这种模式,可暂时先忽略;
- EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;这里我们理解为控件的宽或者高被设置为 match_parent 或者指定大小,比如20dp;
- AT_MOST:子元素至多达到指定大小的值;这里我们理解为控件的宽或者高被设置为wrap_content。
巧妙的是,Android并非通过上述定义MeasureSpec对象的方式对 布局要求 进行描述,而是使用了更简单的二进制的方式,用一个32位的int值进行替代:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30; //移位位数为30
//int类型占32位,向右移位30位,该属性表示掩码值,用来与size和mode进行"&"运算,获取对应值。
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//00左移30位,其值为00 + (30位0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//01左移30位,其值为01 + (30位0)
public static final int EXACTLY = 1 << MODE_SHIFT;
//10左移30位,其值为10 + (30位0)
public static final int AT_MOST = 2 << MODE_SHIFT;
// 根据size和mode,创建一个测量要求
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
// 根据规格提取出mode,
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 根据规格提取出size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
这个int值中,前2位代表了测量模式,后30位则表示了测量的大小,对于模式和大小值的获取,只需要通过位运算即可。
以宽度举例来说,若我们设置宽度=5px(二进制对应了101),那么mode对应EXACTLY,在创建测量要求的时候,只需要通过二进制的相加,便可得到存储了相关信息的int值:
而当需要获得Mode的时候只需要用measureSpec与MODE_TASK相与即可,如下图:
同理,想获得size的话只需要只需要measureSpec与~MODE_TASK相与即可,如下图:
现在读者对MeasureSpec类有了初步地认识,在Android绘制过程中,View宽或者高的 布局要求 实际上是通过32位的int值进行的描述, 而MeasureSpec类本身只是一个静态方法的容器而已。
对于普通View(DecorView略有不同),其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽高。
那么针对不同父容器和View本身不同的LayoutParams,View就可以有多种MeasureSpec,我们从子View的角度来分析。
①View宽/高采用 固定宽高 的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY并且其大小遵循LayoutParams中的大小。
②View宽/高采用 wrap_content 的时候 ,不管父容器的模式是EXACTLY还是AT_MOST,View的模式总是AT_MOST,并且大小不能超过父容器的剩余空间(SpecSize,可用大小)。
③View宽/高采用 match_parent 的时候 :
- 如果父容器的模式是EXACTLY,那么View也是EXACTLY并且其大小是父容器的剩余空间(SpecSize,最终大小);
- 如果父容器的模式是AT_MOST,那么View也是AT_MOST并且其大小不会超过父容器的剩余空间(SpecSize,可用大小)。
我们知道View的测量是从ViewGroup开始递归测量所有的子View,测量完毕之后再测量自己。
而每个View自身的测量过程都包含3个重要的函数,分别为:
- final void measure(int widthMeasureSpec, int heightMeasureSpec):执行测量的函数;
- void onMeasure(int widthMeasureSpec, int heightMeasureSpec):真正执行测量的函数,开发者需要自己实现自定义的测量逻辑;
- final void setMeasuredDimension(int measuredWidth, int measuredHeight):完成测量的函数,保存测量宽高;
同时包含子View的ViewGroup和View的测量过程也有些许不同;
1.measure()入口函数:标记测量的开始
首先父控件需要通过调用子控件的measure()函数,并同时将宽和高的 布局要求 作为参数传入,标志子控件本身测量的开始:
// 这个是父控件的代码,让子控件开始测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
对于View的测量流程,其必然包含了2部分:公共逻辑部分 和 开发者自定义测量的逻辑部分,为了保证公共逻辑部分代码的安全性,设计者将measure()方法配置了final修饰符:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ... 公共逻辑
// 开发者需要自己重写onMeasure函数,以自定义测量逻辑
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
开发者不能重写measure()函数,并将View自定义测量的策略通过定义一个新的onMeasure()接口暴露出来供开发者重写。
//View
//final类,子类不能重写该方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
``````
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
view.measure()
方法其实没有实现任何测量的算法,它的作用在于判断是否需要引发onMeasure()
的调用,并对onMeasure()
行为的正确性进行检查。
2.onMeasure:用来自定义View的测量策略
不同ViewGroup实现类有不同的测量方式,例如LinearLayout自身的高度是子View高度的累加。
在FrameLayout的onMeasure()方法中,会通过measureChildWithMargins()方法遍历子View,并且如果FrameLayout宽高的MeasureSpec是AT_MOST,那么FrameLayout计算自身宽高就会受到子View的影响,可能使用最大子View的宽高。DecorView是整个View树的根View,DecorView继承自FrameLayout,绘制流程也是从DecorView开始的。
//FrameLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//遍历子View,只要子View不是GONE便处理
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//子View结合父View的MeasureSpec和自己的LayoutParams算出子View自己的MeasureSpec
//如果当前child也是ViewGroup,也继续遍历它的子View
//如果当前child是View,便根据这个MeasureSpec测量自己
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
``````
}
}
``````
//父View等所有的子View测量结束之后,再来测量自己
setMeasuredDimension(``````);
}
measureChildWithMargins()方法为ViewGroup提供的方法,根据父View的MeasureSpec和子View的LayoutParams,算出子View自己的MeasureSpec。
//ViewGroup
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public static int getChildMeasureSpec(int spec, int padding, int childDimesion) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// padding是指父容器中已占用的空间大小,因此子元素可用的
// 大小为父容器的尺寸减去padding
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (sepcMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimesion == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size....
// find out how big it should be
resultSize = 0;
resultMode == MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。显然子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的margin和padding有关。
回顾一下上面的measure段落,因为具体实现的ViewGroup会重写onMeasure(),因为ViewGroup是一个抽象类,其测量过程的onMeasure()方法需要具体实现类去实现,这么做的原因在于不同的ViewGroup实现类有不同的布局特性,比如说FrameLayout,RelativeLayout,LinearLayout都有不同的布局特性,因此ViewGroup无法对onMeasure()做同一处理。
View自身也提供了onMeasure()函数默认的测量策略:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
以宽度为例,通过这样获取View默认的宽度:
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
1.在某些情况下(比如自身设置了minWidth或者background属性),View需要通过getSuggestedMinimumWidth()函数作为默认的宽度值:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
2.这之后,将所得结果作为参数传递到getDefaultSize(minWidth, widthMeasureSpec)函数中,根据 布局要求 计算出View最后测量的宽度值:
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;
// match_parent、wrap_content则返回布局要求中的size值
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
从getDefaultSize()方法的实现来看,对于AT_MOST和EXACTLY这两种情况View的宽高都由specSize决定,也就是说如果我们直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身的大小,否则在布局中使用wrap_content就相当于使用match_parent。
3.setMeasuredDimension():保存测量数据
setMeasuredDimension(width,height)函数的存在意义非常重要,在onMeasure()执行自定义测量策略的过程中,调用该函数标志着View的测量得出了结果:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 普遍意义上,setMeasuredDimension()标志着测量结束
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// measuredWidth 测量结果,View的宽度
// measuredHeight 测量结果,View的高度
// 省略其它代码...
// 该方法的本质就是将测量结果存起来,以便后续的layout和draw流程中获取控件的宽高
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
}
该函数被设计为由protected final修饰,这意味着只能由子类进行调用而不能重写。函数调用完毕,开发者可以通过getMeasuredWidth()或者getMeasuredHeight()来获取View测量的宽高,代码设计大概是这样:
public final int getMeasuredWidth() {
return mMeasuredWidth;
}
public final int getMeasuredHeight() {
return mMeasuredHeight;
}
// 如何使用
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight()
经过measure() -> onMeasure() -> setMeasuredDimension()函数的调用,最终View自身测量流程执行完毕。
3.layout布局
子View具体layout的位置都是相对于父容器而言的,View的layout过程和measure同理,也是从顶级View开始,递归的完成整个空间树的布局操作。
经过前面的测量,控件树中的控件对于自己的尺寸显然已经了然于胸。而且父控件对于子控件的位置也有了眉目,所以经过测量过程后,布局阶段会把测量结果转化为控件的实际位置与尺寸。控件的实际位置与尺寸由View的mLeft,mTop,mRight,mBottom 等4个成员变量存储的坐标值来表示。
并且需要注意的是: View的mLeft,mTop,mRight,mBottom 这些坐标值是以父控件左上角为坐标原点进行计算的。倘若需要获取控件在窗口坐标系中的位置可以使用View.getLocationInWindow()或者是View.getRawX()/Y()。
//ViewRootImpl
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {
final View host = mView;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
//ViewGroup
//尽管ViewGroup也重写了layout方法
//但是本质上还是会通过super.layout()调用View的layout()方法
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
//如果无动画,或者动画未运行
super.layout(l, t, r, b);
} else {
//等待动画完成时再调用requestLayout()
mLayoutCalledWhileSuppressed = true;
}
}
//View
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//如果布局有变化,通过setFrame重新布局
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//如果这是一个ViewGroup,还会遍历子View的layout()方法
//如果是普通View,通知具体实现类布局变更通知
onLayout(changed, l, t, r, b);
//清除PFLAG_LAYOUT_REQUIRED标记
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
``````
//布局监听通知
}
//清除PFLAG_FORCE_LAYOUT标记
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
}
通过代码可以看到尽管ViewGroup也重写了layout()方法,但是本质上还是会走View的layout()。
在View的layout()方法里,首先通过 setFrame()(setOpticalFrame()也走setFrame())将l、t、r、b分别设置到mLeft、mTop、mRight、和mBottom,这样就可以确定子View在父容器的位置了,上面也说过了,这些位置是相对父容器的。
然后调用onLayout()方法,使具体实现类接收到布局变更通知。如果此类是ViewGroup,还会遍历子View的layout()方法使其更新布局。
//View
protected void onLayout(boolean changed, int l, int t, int r, int b) {}
//ViewGroup
//abstract修饰,具体实现类必须重写该方法
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
对于普通View来说,onLayout()方法是一个空实现,主要是具体实现类重写该方法后能够接收到布局坐标更新信息。
对于ViewGroup来说,和measure一样,不同实现类有它不同的布局特性,在ViewGroup中onLayout()方法是abstract的,具体实现类必须重写该方法,以便接收布局坐标更新信息后,处理自己的子View的坐标信息。
//FrameLayout.java
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
小结
对比测量measure和布局layout两个过程有助于加深对它们的理解。
- measure确定的是控件的尺寸,并在一定程度上确定了子控件的位置。而布局则是针对测量结果来实施,并最终确定子控件的位置。
- measure结果对布局过程没有约束力。虽说子控件在onMeasure()方法中计算出了自己应有的尺寸,但是由于layout()方法是由父控件调用,因此控件的位置尺寸的最终决定权掌握在父控件手中,测量结果仅仅只是一个参考。
- 因为measure过程是后根遍历(DecorView最后setMeasureDiemension()),所以子控件的测量结果影响父控件的测量结果。
- 而Layout过程是先根遍历(layout()一开始就调用setFrame()完成DecorView的布局),所以父控件的布局结果会影响子控件的布局结果。
4.draw绘制
View的draw过程相对上面measure和layout比较简单。
View的draw过程遵循如下几步 :
绘制背景drawBackground();
绘制自己onDraw();
如果是ViewGroup则绘制子View,dispatchDraw();
绘制装饰(滚动条)和前景,onDrawForeground();
//View
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
//检查是否是"实心(不透明)"控件。(后面有补充)
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* 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);//如果当前不是ViewGroup,此方法则是空实现
// 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;
}
``````
}
5.核心方法
requestLayout、invalidate与postInvalidate
//View
public void invalidate() {
invalidate(true);
}
void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
//如果VIew不可见,或者在动画中
if (skipInvalidate()) {
return;
}
//根据mPrivateFlags来标记是否重绘
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {//上面传入为true,表示需要全部重绘
mLastIsOpaque = isOpaque();//
mPrivateFlags &= ~PFLAG_DRAWN;//去除绘制完毕标记。
}
//添加脏区标记。
mPrivateFlags |= PFLAG_DIRTY;
//清除缓存,表示由当前View发起的重绘。
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
//把需要重绘的区域传递给父View
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
//设置重绘区域(区域为当前View在父容器中的整个布局)
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
``````
}
}
上述代码中,会设置一系列的标记位到mPrivateFlags
中,并且通过父容器的invalidateChild
方法,将需要重绘的脏区域传给父容器。(ViewGroup和ViewRootImpl都继承了ViewParent类,该类中定义了子元素与父容器间的调用规范。)
//ViewGroup
@Override
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
``````
//父容器根据自身对子View的脏区域进行调整
transformMatrix.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
// 这里的do while方法,不断的去调用父类的invalidateChildInParent方法来传递重绘请求
//直到调用到ViewRootImpl的invalidateChildInParent(责任链模式)
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
//如果父类是"实心"的,那么设置它的mPrivateFlags标识
// If the parent is dirty opaque or not dirty, mark it dirty with the opaque
// flag coming from the child that initiated the invalidate
if (view != null) {
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
//***往上递归调用父类的invalidateChildInParent***
parent = parent.invalidateChildInParent(location, dirty);
//设置父类的脏区域
//父容器会把子View的脏区域转化为父容器中的坐标区域
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
}
}
while (parent != null);
}
}
由于最上层的ViewParent是ViewRootImpl,所以我们可以查看ViewRootImpl的invalidateChildInParent
方法即可。
//ViewRootImpl
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
//检查线程,这也是为什么invalidate一定要在主线程的原因
checkThread();
if (dirty == null) {
invalidate();//有可能需要绘制整个窗口
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
``````
invalidateRectOnScreen(dirty);
return null;
}
//设置mDirty并执行View的工作流程
private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
mAttachInfo.mSetIgnoreDirtyState = true;
mAttachInfo.mIgnoreDirtyState = true;
}
// Add the new dirty rect to the current one
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
//在这里,mDirty的区域就变为方法中的dirty,即要重绘的脏区域
``````
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();//执行View的工作流程
}
}
invalidate()方法也调用到了scheduleTraversals()!
因此也会执行View的绘制流程perforMeasure()、performLayout()、perforDraw()。
这个scheduleTraversals()很眼熟,在setView()->requestLayout()中见过,并且我们还说了mLayoutRequested用来表示是否measure和layout。
//ViewRootImpl
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();//检查是否在主线程
mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
scheduleTraversals();
}
}
private void performTraversals() {
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
measureHierarchy(```);//measure
}
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
performLayout(lp, mWidth, mHeight);//layout
}
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
performDraw();//draw
}
}
因为我们invalidate
的时候,并没有设置mLayoutRequested
,所以它只走performDraw()
流程,并且在draw()
流程中会清除mDirty区域。
postInvalidate
首先看postInvalidate的实现
public void postInvalidate() {
postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
会调用到ViewRootImpl的dispatchInvalidateDelayed方法,就是用handler在非主线程发送了一个异步消息
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
invalidate是在主线程调用,postInvalidate在非主线程中调用,这就是它们的去别。
requestLayout
在view.measure()的方法里,仅当给与的MeasureSpec发生变化时,或要求强制重新布局时,才会进行测量。
强制重新布局 : 控件树中的一个子控件内容发生变化时,需要重新测量和布局的情况,在这种情况下,这个子控件的父控件(以及父控件的父控件)所提供的MeasureSpec必定与上次测量时的值相同,因而导致从ViewRootImpl到这个控件的路径上,父控件的measure()方法无法得到执行,进而导致子控件无法重新测量其布局和尺寸。(在父容器measure中遍历子元素)
解决途径 : 因此,当子控件因内容发生变化时,从子控件到父控件回溯到ViewRootImpl,并依次调用父控件的requestLayout()方法。这个方法会在mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT,从而使得这些父控件的measure()方法得以顺利执行,进而这个子控件有机会进行重新布局与测量。这便是强制重新布局的意义所在。
//View
@CallSuper
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
``````
// 增加PFLAG_FORCE_LAYOUT标记,在measure时会校验此属性
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
// 父类不为空&&父类没有请求重新布局(是否有PFLAG_FORCE_LAYOUT标志)
//这样同一个父容器的多个子View同时调用requestLayout()就不会增加开销
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
最顶层的ViewParent是ViewRootImpl。
//ViewRootImpl
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
同样,requestLayout()方法会调用scheduleTraversals(),因为设置了mLayoutRequested =true标识,所以在performTraversals()中调用performMeasure(),performLayout(),但是由于没有设置mDirty,所以不会走performDraw()流程。
但是,requestLayout()方法就一定不会导致onDraw()的调用吗?
在上面layout()方法中说道 :
在View的layout()方法里,首先通过 setFrame()(setOpticalFrame()也走setFrame())将l、t、r、b分别设置到mLeft、mTop、mRight、和mBottom,这样就可以确定子View在父容器的位置了,上面也说过了,这些位置是相对父容器的。
//View --> layout()
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
//布局坐标改变了
changed = true;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);//调用invalidate重新绘制视图
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
``````
}
return changed;
}