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_MASK相与即可,如下图:
同理,想获得size的话只需要只需要measureSpec与~MODE_MASK相与即可,如下图:
现在读者对MeasureSpec类有了初步地认识,在Android绘制过程中,View宽或者高的 布局要求 实际上是通过32位的int值进行的描述, 而MeasureSpec类本身只是一个静态方法的容器而已。
对于普通View(DecorView略有不同),其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,MeasureSpec一旦确定后,o