Android图形系统(三)-View绘制流程

接上篇 绘制优化-原理篇2-DecorView布局加载流程 讲到的ViewRootImpl,在ViewRootImpl的setView()方法里主要做两件事:
1.执行requestLayout()方法完成view的绘制流程
2.通过WindowSession将View和InputChannel添加到WmS中,从而将View添加到Window上并且接收触摸事件。

2的部分 window加载视图已经介绍了,那么今天就来讲讲1的部分:执行requestLayout()方法完成view的绘制流程

//ViewRootImpl
  public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                 //在 Window add之前调用,确保 UI 布局绘制完成 --> measure , layout , draw
                requestLayout();//View的绘制流程
                ...
                    //通过WindowSession进行IPC调用,将View添加到Window上
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                }
                ...
    }

一、从requestLayout开始

从requestLayout代码一层层往下追(具体源码不贴了,非常简单),最终确认view的绘制流程是从performTraversals开始。顺一下整个流程:

1.1 performTraversals
private void performTraversals() {
     //获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.width和lp.height表示DecorView根布局宽和高
     WindowManager.LayoutParams lp = mWindowAttributes;
     ...
        //顶层视图DecorView所需要窗口的宽度和高度
        int desiredWindowWidth;
        int desiredWindowHeight;
     ...
        //在构造方法中mFirst已经设置为true,表示是否是第一次绘制DecorView
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            //如果窗口的类型是有状态栏的,那么顶层视图DecorView所需要窗口的宽度和高度就是除了状态栏
            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {//否则顶层视图DecorView所需要窗口的宽度和高度就是整个屏幕的宽高
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }
    }
    ...
     int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
     int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
     ...
     // Ask host how big it wants to be
     //执行测量操作
     performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
      ...
     //执行布局操作
    performLayout(lp, mWidth, mHeight);
     ...
     //执行绘制操作
     performDraw();
}

performTraversals()中做了非常多的处理,代码接近800行,这里我们重点关注绘制相关流程。

1.2 MeasureSpec

在分析绘制过程之前,我们需要先了解MeasureSpec,它是干什么的呢?简而言之,MeasureSpec 是View的尺寸一种封装手段。

MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SepcSize. 这样的打包方式好处是避免过多的对象内存分配。为了方便操作,其提供了打包和解包的方法:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;
...
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                  @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}
...
public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK); //高2位运算
        }
public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);//低30位运算
        }
}

getMode方法中ModeMask 为 0x3 << 30 转换成二进制为 0011 << 30 ,也就是向左移动30位 则 ModeMask高两位为1,低三十位为0,整形measureSpec 为32位, measureSpec & mode_mask 就是高2位的运算,getSize方法中 ~Mode_MASK 则是除了高两位为0 外剩下的低30位均为 1,那么和measure 进行 & 运算就是在求得低30为中存储的值。

SpecMode:测量模式

模式 描述
UNSPECIFIED 父容器不作限制,一般用于系统内部
EXACTLY 精确模式,大小为SpecSize,对应LayoutParams中的match_parent或者具体数值
AT_MOST 最大模式,大小不能大于SpecSize,对应于LayoutParams中的warp_content

SpecSize:对应某种测量模式下的尺寸大小

下面针对DecorView和普通View分别来看看其MeasureSpec的组成:

//ViewRootImpl
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
 DecorView, 其MeasureSpec由窗口尺寸和其自身LayoutParams共同决定,
}

//ViewGroup
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
   普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。
}

对普通View的MeasureSpec的创建规则进行总结:

图片摘自Android开发艺术探索

这个表怎么用呢?举个例子:
如果View在布局中使用wrap_content,那么它的specMode是AT_MOST,这种模式下,它的宽高为specSize, 而查表可得View的specSize是parentSize,而parentSize是当前父容器剩余空间大小,这种效果和在布局中使用match_parent完全一致,所以如果是对尺寸有具体要求的自定义控件需要指定specSize大小。

注:
LayoutParams类是用于子视图向父视图传达自己尺寸意愿的一个参数包,包含了Layout的高、宽信息。LayoutParams在LayoutInflater.inflater过程中与View一起被解析成对象,保存在WindowManagerGlobal集合中。

二、View绘制流程

performTraversals里面执行了三个方法,分别是performMeasure()、performLayout()、performDraw()这三个方法,这三个方法分别完成DecorView的measure、layout、和draw这三大流程,其中performMeasure()中会调用measure()方法,在measure()方法中又会调用onMeasure()方法,在onMeasure()方法中会对所有子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就实现了从DecorView开始对整个View树的遍历测量,measure过程就这样完成了。同理,performLayout()和performDraw()也是类似的传递流程。针对performTraveals()的大致流程,可以用以下流程图来表示:

from:https://www.jianshu.com/p/4a68f9dc8f7c

以上的流程图只是一个为了便于理解而简化版的流程,真正的流程应该分为以下五个工作阶段:

  • 预测量阶段:这是进入performTraversals()方法后的第一个阶段,它会对View树进行第一次测量。在此阶段中将会计算出View树为显示其内容所需的尺寸,即期望的窗口尺寸。(调用measureHierarchy())

  • 窗口布局阶段:根据预测量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口进行重新布局,并将布局结果返回给ViewRootImpl。(调用relayoutWindow())

  • 测量阶段:预测量的结果是View树所期望的窗口尺寸。然而由于在WMS中影响窗口布局的因素很多,WMS不一定会将窗口准确地布局为View树所要求的尺寸,而迫于WMS作为系统服务的强势地位,View树不得不接受WMS的布局结果。因此在这一阶段,performTraversals()将以窗口的实际尺寸对View树进行最终测量。(调用performMeasure())

  • 布局阶段:完成最终测量之后便可以对View树进行布局了。(调用performLayout())

  • 绘制阶段:这是performTraversals()的最终阶段。确定了控件的位置与尺寸后,便可以对View树进行绘制了。(调用performDraw())

下面分别来阐述:

2.1 预测量阶段(performTraversals())

这个阶段在performTraversals中最先发生,对View树进行第一次测量,会判断当前期望窗口尺寸是否能满足布局要求。

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        // 表示测量结果是否可能导致窗口的尺寸发生变化
        boolean windowSizeMayChange = false;
        //goodMeasure表示了测量是否能满足View树充分显示内容的要求
        boolean goodMeasure = false;
        //测量协商仅发生在LayoutParams.width被指定为WRAP_CONTENT的情况下
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            //第一次协商。measureHierarchy()使用它最期望的宽度限制进行测量。
            //这一宽度限制定义为一个系统资源。
            //可以在frameworks/base/core/res/res/values/config.xml找到它的定义
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            // 宽度限制被存放在baseSize中
            int baseSize = 0;
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                //第一次测量。调用performMeasure()进行测量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                //View树的测量结果可以通过mView的getmeasuredWidthAndState()方法获取。
                //View树对这个测量结果不满意,则会在返回值中添加MEASURED_STATE_TOO_SMALL位
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    goodMeasure = true;  // 控件树对测量结果满意,测量完成
                } else {
                  //第二次协商。上次的测量结果表明View树认为measureHierarchy()给予的宽度太小,在此
                  //在此适当地放宽对宽度的限制,使用最大宽度与期望宽度的中间值作为宽度限制
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                  //第二次测量
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                  // 再次检查控件树是否满足此次测量
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                     // 控件树对测量结果满意,测量完成
                        goodMeasure = true;
                    }
                }
            }
        }
        if (!goodMeasure) {
          //最终测量。当View树对上述两次协商的结果都不满意时,measureHierarchy()放弃所有限制
          //做最终测量。这一次将不再检查控件树是否满意了,因为即便其不满意,measurehierarchy()也没
          //有更多的空间供其使用了
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
          //如果测量结果与ViewRootImpl中当前的窗口尺寸不一致,则表明随后可能有必要进行窗口尺寸的调整
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }
        // 返回窗口尺寸是否可能需要发生变化
        return windowSizeMayChange;
    }

measureHierarchy()方法最终也是调用了performMeasure()方法对View树进行测量,只是多了协商测量的过程。

2.2 窗口布局阶段(relayoutWindow())

调用relayoutWindow()来请求WindowManagerService服务计算Activity窗口的大小以及过扫描区域边衬大小和可见区域边衬大小。计算完毕之后,Activity窗口的大小就会保存在成员变量mWinFrame中,而Activity窗口的内容区域边衬大小和可见区域边衬大小分别保存在ViewRoot类的成员变量mPendingOverscanInsets和mPendingVisibleInsets中。

这部分不在这细讲了,有耐心的可以把老罗的文章看完:Android窗口管理服务WindowManagerService计算Activity窗口大小的过程分析

2.3 测量过程(performMeasure())

WMS的布局结果已经确定了,不管是否满意都得开始终极布局过程了,下面介绍下measure:
measure是对View进程测量,确定各View的尺寸的过程,这个过程分View和ViewGroup两种情况来看,对于View,通过measure完成自身的测量就行了,而ViewGroup除了完成自身的测量外,还需要遍历去调用所有子view的measure方法,各个子view递归去执行这个过程。

那么先从performMeasure开始:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

这里的mView是 ViewRootImpl setView传进来的rootView.
接下来我们看下View的measure()方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
     ...
    //根据widthMeasureSpec和heightMeasureSpec计算key值,在下面用key值作为键,缓存我们测量得到的结果
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
    …
    //forceLayout 是通过上次的mPrivateFlags标记位来判断这次是否需要触发重绘
    //View中有个forceLayout()方法可以设置mPrivateFlags.
   // needsLayout 简单看就是spec发生了某些规则约束下的变化
    if (forceLayout || needsLayout) {
        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
        resolveRtlPropertiesIfNeeded();
        //在View真正进行测量之前,View还想进一步确认能不能从已有的缓存mMeasureCache中读取缓存过的测量结果 //如果是强制layout导致的测量,那么将cacheIndex设置为-1,即不从缓存中读取测量结果 //如果不是强制layout导致的测量,那么我们就用上面根据measureSpec计算出来的key值作为缓存索引cacheIndex,这时候有可能找到相应的值,找到就返回对应索引;也可能找不到,找不到就返回-1
        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            //在缓存中找不到相应的值或者需要忽略缓存结果的时候,重新测量一次 //此处调用onMeasure方法,并把尺寸限 制条件widthMeasureSpec和heightMeasureSpec传入进去 //onMeasure方法中将会进行实际的测量工作,并把测量的结果保存到成员变量中
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            //如果运行到此处,那么表示当前的条件允许View从缓存成员变量mMeasureCache中读取测量过的结果
           //用上面得到的cacheIndex从缓存mMeasureCache中取出值,不必在调用onMeasure方法进行测量了
            long value = mMeasureCache.valueAt(cacheIndex);
          //一旦我们从缓存中读到值,我们就可以调用setMeasuredDimensionRaw方法将当前测量的结果保存到成员变量中        
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
          ...
        }
      ...
    }
    ...
}

先判断一下是否有必要进行测量操作,如果有,先看是否能在缓存mMeasureCache中找到上次的测量结果,如果找到了那直接从缓存中获取就可以了,如果找不到,那么乖乖地调用onMeasure()方法去完成实际的测量工作,并且将尺寸限制条件widthMeasureSpec和heightMeasureSpec传递给onMeasure()方法。

另外,measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架. 主要看onMeasure()方法,这里才是真正去测量并设置View大小的地方。

2.3.1 View的measure过程:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension方法会设置View宽高的测量值,因此主要看下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;
}

很显然看出:
AT_MOST 和 EXACTLY两种情况返回的就是specSize。

UNSPECIFIED返回的是size, 即getSuggestedMinimumWidth 和 getSuggestedMinimumHeight的返回值。

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

mMinWidth 对应于android:minWidth属性指定的值。总结:如果View没有设置背景,那么返回mMinWidth的值,否则返回mMinWidth和背景最小宽度的最大值。

2.3.2 ViewgGroup的measure过程:

ViewGroup 继承自 View,我们知道View的 measure是 final方法,那这个方法是肯定会走的,但是具体实现是在onMeasure中,ViewGroup提供了几个方法来帮助ViewGroup的子类来实现onMeasure逻辑,包括:

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);
}

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);
}

仔细看其实最终还是让child去执行自己对于的measure,只是getChildMeasureSpec有差别,这里加上了margin 和 padding.

具体onMeasure的实现可以参考LinearLayout、FrameLayout、RelativeLayout等。

另外需要关注的是ViewGroup 的 getChildMeasureSpec方法,我们从上面代码中很明显看出,传入的Spec是父容器的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) {
   // 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 (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 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 = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
           resultMode = MeasureSpec.UNSPECIFIED;
       } else if (childDimension == LayoutParams.WRAP_CONTENT) {
           // Child wants to determine its own size.... find out how
           // big it should be
           resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
           resultMode = MeasureSpec.UNSPECIFIED;
       }
       break;
   }
   //noinspection ResourceType
   return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

很明显看出来,对于普通View来说,getChildMeasureSpec的逻辑是通过其父View提供的MeasureSpec参数得到specMode和specSize,然后根据计算出来的specMode以及子View的childDimension(layout_width或layout_height)来计算自身的measureSpec 。

measure总结:

  • MeasureSpec 由specMode和specSize组成:
    DecorView, 其MeasureSpec由窗口尺寸和其自身LayoutParams共同决定。
    普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。

  • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

  • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,以及getChildMeasureSpec方法 ,供具体实现ViewGroup的子类重写onMeasure的时候方便使用。

  • 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。
    比较常用的方式:
    view.post(runnable)
    view.measure(0,0)之后 get

measure整体执行流程:

form: 工匠若水
2.4 布局过程 (performLayout())

Layout的作用是ViewGroup用来确定子view的位置,当ViewGroup的位置被确定之后,它在onLayout中会遍历所有子view并调用其layout方法,在layout方法中onLayout又被调用。

先从performLayout看起:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
           int desiredWindowHeight) {
       ...
       //标记当前开始布局
       mInLayout = true;
       //mView就是DecorView
       final View host = mView;
       ...
       //DecorView请求布局 layout参数分别是 左 上 右 下
       host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
       //标记布局结束
       mInLayout = false;
       ...
}

跟踪代码进入View类的layout方法

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;
       }
       //保存上一次View的四个位置
       int oldL = mLeft;
       int oldT = mTop;
       int oldB = mBottom;
       int oldR = mRight;
       //设置当前视图View的左,顶,右,底的位置,并且判断布局是否有改变
       boolean changed = isLayoutModeOptical(mParent) ?
               setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
       //如果布局有改变,条件成立,则视图View重新布局
           if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
           //调用onLayout,将具体布局逻辑留给子类实现
           onLayout(changed, l, t, r, b);
           mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
           ListenerInfo li = mListenerInfo;
           if (li != null && li.mOnLayoutChangeListeners != null) {
               ArrayList<OnLayoutChangeListener> listenersCopy =
                       (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
               int numListeners = listenersCopy.size();
               for (int i = 0; i < numListeners; ++i) {
                   listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
               }
           }
       }
       mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
       mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
   }

首先关注下需要重新layout的条件:

boolean changed = isLayoutModeOptical(mParent) ?
               setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

其中setOpticalFrame内部也会调用setFrame,所以就看下setFrame好了:

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;
        // Remember our drawn bit
        int drawn = mPrivateFlags & PFLAG_DRAWN;
        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);
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
       ...
    }
    return changed;
}

通过setFrame方法设定View的四个顶点的位置,并更新本地值,同时判断顶点位置较之前是否有变化,并return是否有变化的boolean值,如果有变化还会执行invalidate(sizeChanged)。

然后,咱们再看看onLayout方法:
View中的onLayout是个空方法,可实现可不实现

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

ViewGroup中是个抽象方法,子类必须实现

@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

下面以RelativeLayout为例,对onLayout具体实现做简单的分析:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
   //  The layout has actually already been performed and the positions
   //  cached.  Apply the cached values to the children.
   final int count = getChildCount();
   for (int i = 0; i < count; i++) {
       View child = getChildAt(i);
       if (child.getVisibility() != GONE) {//只有不为GONE的才会执行布局
           RelativeLayout.LayoutParams st =
                   (RelativeLayout.LayoutParams) child.getLayoutParams();
           child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
       }
   }
}

遍历所有子view,并通过其LayoutParams 获取四个方向的位置值,将位置信息传入子view的layout方法进行布局。

layout总结:

  • Layout的作用是ViewGroup用来确定子view的位置, 是ViewGroup需要干的活,View不需要,所以View中是空方法,而ViewGroup中是抽象方法,但是View你也可以重写,大多数是利用这个生命周期阶段加写逻辑操作。

  • 当我们的视图View在布局中使用 android:visibility=”gone” 属性时,是不占据屏幕空间的,因为在布局时ViewGroup会遍历每个子视图View,判断当前子视图View是否设置了 Visibility==GONE,如果设置了,当前子视图View就不会添加到父容器上,因此也就不占据屏幕空间。具体可以参考RelativeLayout的onLayout.

  • 必须在View布局完之后调用getHeight( )和getWidth( )方法获取到的View的宽高才大于0.

layout的整体执行流程:

from:工匠若水
2.5 绘制过程 (performDraw())

Draw作用是将View绘制到屏幕上.过程相对比较简单。
draw是从performDraw开始

//ViewRootImpl
private void performDraw() {
       ...
       draw(fullRedrawNeeded);
       ...
}

然后看ViewRootImpl的draw方法:

 //ViewRootImpl
   private void draw(boolean fullRedrawNeeded) {
        Surface surface = mSurface;
     ...
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
     ...
    }

再看ViewRootImpl的drawSoftware方法:

 //ViewRootImpl
  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
...
 mView.draw(canvas);
...
}

最终在drawSoftware方法中,会走到View的draw并传入了canvas画布。这部分先不细说,之后的Surface部分会分析。

那么接着往下的话就是真正View绘制的部分了:

   //View
   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
       ......
       if (!dirtyOpaque) {
           drawBackground(canvas);
       }
       // skip step 2 & 5 if possible (common case)
       ......
       // Step 2, save the canvas' layers
       ......
           if (drawTop) {
               canvas.saveLayer(left, top, right, top + length, null, flags);
           }
       ......
       // Step 3, draw the content
       if (!dirtyOpaque) onDraw(canvas);
       // Step 4, draw the children
       dispatchDraw(canvas);
       // Step 5, draw the fade effect and restore layers
       ......
       if (drawTop) {
           matrix.setScale(1, fadeHeight * topFadeStrength);
           matrix.postTranslate(left, top);
           fade.setLocalMatrix(matrix);
           p.setShader(fade);
           canvas.drawRect(left, top, right, top + length, p);
       }
       ......
       // Step 6, draw decorations (scrollbars)
       onDrawScrollBars(canvas);
       ......
   }

从摘要可以看出,绘制过程分如下几步:

  • 绘制背景 background.draw(canvas)
   private void drawBackground(Canvas canvas) {
       //获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable
       final Drawable background = mBackground;
       ......
       //根据layout过程确定的View位置来设置背景的绘制区域
       if (mBackgroundSizeChanged) {
           background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
           mBackgroundSizeChanged = false;
           rebuildOutline();
       }
       ......
           //调用Drawable的draw()方法来完成背景的绘制工作
           background.draw(canvas);
       ......
   }
  • 绘制自己(onDraw)
    View中onDraw是一个空方法,ViewGroup也没有重新实现。
protected void onDraw(Canvas canvas) {
}
  • 绘制children(dispatchDraw)
    View的dispatchDraw()方法是一个空方法,而且注释说明了如果View包含子类需要重写他。所以ViewGroup肯定重写了,来看看:
   @Override
    protected void dispatchDraw(Canvas canvas) {
        ......
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        ......
        for (int i = 0; i < childrenCount; i++) {
            ......
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            ......
            for (int i = disappearingCount; i >= 0; i--) {
                ......
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
    }

可以看见,ViewGroup确实重写了View的dispatchDraw()方法,该方法内部会遍历每个子View,然后调用drawChild()方法,我们可以看下ViewGroup的drawChild方法,如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}
  • 绘制装饰(onDrawScrollBars)

可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。这部分不做详细分析了。

draw拓展点:
1)View的setWillNotDraw方法:
如果一个View不需要绘制任何内容,那么设置这个标记位为true后,系统会进行相应的优化,如果需要通过onDraw绘制内容,则需要设置为false。(默认View是false ViewGroup是true)这是个优化手段。

2)区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

3)默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

draw整体执行流程:


from:工匠若水

三、forceLayout 、invalidate 、requestLayout简述

在之前分析的绘制流程中,我们或多或少都见过这三个方法,他们到底是干什么的,下面做下简单说明:

View#forceLayout( )
public void forceLayout() {
    if (mMeasureCache != null) mMeasureCache.clear();
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;
}

官方描述:强制此视图在下一次布局传递期间进行布局。此方法不调用父类的requestLayout()或forceLayout()。

每个View都有个成员变量:mPrivateFlags,在不同的绘制执行路径会对它赋值。在measure方法内部forceLayout用来判断是否执行onMeasure:

final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
View#invalidate( ) 和 View#postInvalidate( )

invalidate和postInvalidate:都是用来重绘View,区别就是invalidate只能在主线程中调用,postInvalidate可以在子线程中调用.

View#requestLayout

requestLayout: 当前view及其以上的viewGroup部分都重新走ViewRootImpl 重新绘制 ,分别重新onMeasure onLayout onDraw ,其中onDraw比较特殊,有内容变化才会触发。

最后一张图总结下invalidate/postInvalidate 和 requestLayout

参考:
https://blog.csdn.net/yanbober/article/details/46128379
https://blog.csdn.net/feiduclear_up/article/details/46772477
https://www.jianshu.com/p/4a68f9dc8f7c
https://www.jianshu.com/p/a65861e946cb
《Android开发艺术探索》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343