View的测量布局绘制过程

View的测量布局绘制过程本章就重点分析此四法 前置分析(下面代码位于measureHierarchy之前) 接下来看shouldUseDisplaySize(WindowManager.LayoutParams) 方法很ea

在上一篇文章View的显示过程末尾,重点提到了ViewRootImpl的四个方法:

private void performTraversals() {
    ...
    //协商测量
    measureHierarchy
    ...
    //测量
    performMeasure();
    ...
    //布局
    performLayout();
    ...
    //绘制
    performDraw();
}

本章就重点分析此四法

前置分析(下面代码位于measureHierarchy之前)

performTraversals(){
    //这个View就是DecorView
    final View host = mView;
    //本轮期望宽度
    int desiredWindowWidth;
    //本轮期望高度
    int desiredWindowHeight;
    //window大小是否改变,直接影响是否执行performMeasure();
    boolean windowSizeMayChange = false;
    if (mFirst) { //如果是第一次测量,需要走全部流程
        //是否需要完全重新绘制,如果需要,那么后续绘制会全部绘制,否则只绘制需要绘制的局部
        mFullRedrawNeeded = true;
        //是否需要重新布局,如果需要重新布局,那么就会触发协商测量
        mLayoutRequested = true;

        final Configuration config = mContext.getResources().getConfiguration();
        //判断是否是系统窗口:包括从状态栏下拉、键盘弹出、音量调整(下面有这个方法)
        if (shouldUseDisplaySize(lp)) {
            Point size = new Point();
            mDisplay.getRealSize(size);//如果是系统窗口,则使用逻辑尺寸(下面有这个方法)
            desiredWindowWidth = size.x;
            desiredWindowHeight = size.y;
        } else {
            //不是系统窗口,直接使用窗口的大小,也就是物理尺寸
            desiredWindowWidth = mWinFrame.width();
            desiredWindowHeight = mWinFrame.height();
        }
        //...省略部分代码
    } else {
        //如果不是第一次,就尝试使用上一次的尺寸
        desiredWindowWidth = frame.width();
        desiredWindowHeight = frame.height();
        //如果本次期望尺寸和上次尺寸不一样,则需要重新测量
        if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
            //所以这里重置几个标记
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            windowSizeMayChange = true;
        }
    }

    //这里有个切入代码1!!!

    ...
    //协商测量
    measureHierarchy
    ...
    //测量
    performMeaure();
    ...
    //布局
    performLayout();
    ...
    //绘制
    performDraw();
}

接下来看shouldUseDisplaySize(WindowManager.LayoutParams)

private static boolean shouldUseDisplaySize(final WindowManager.LayoutParams lp) {
    return lp.type == TYPE_STATUS_BAR_PANEL //状态栏
            || lp.type == TYPE_INPUT_METHOD //输入框
            || lp.type == TYPE_VOLUME_OVERLAY; //音量调整框
}

方法很easy,就是判断window类型是否是:状态栏/输入框/音量调整框。 接下来看mDisplay.getRealSize(Point)

public void getRealSize(Point outSize) {
    synchronized (this) {
        updateDisplayInfoLocked();
        //这里直接使用了逻辑尺寸,什么是逻辑尺寸,看下面
        outSize.x = mDisplayInfo.logicalWidth;
        outSize.y = mDisplayInfo.logicalHeight;
    }
}

Display中关于逻辑尺寸的说明:

/** * The logical width of the display, in pixels. * Represents the usable size of the display which may be smaller than the * physical size when the system is emulating a smaller display. */
@UnsupportedAppUsage
public int logicalWidth;

/** * The logical height of the display, in pixels. * Represents the usable size of the display which may be smaller than the * physical size when the system is emulating a smaller display. */
@UnsupportedAppUsage
public int logicalHeight;

简单意思就是:这玩意可能比物理尺寸小点!这个是系统使用的,我们做App的不需要关心,知道就行.

小结:

  • 1 如果是第一次测量,那么先判断是否是系统的特定窗口,如果是,则使用可能小一点的逻辑尺寸,否则直接使用窗口尺寸,同时设置需要完整绘制和重新布局的标记
  • 2 如果不是第一次测量,则先取上一次的尺寸来看是否满足本次期望尺寸,如果不满足,就重置需要完整绘制和重新布局的标记,并且重置需要测量的标记

measureHierarchy() 协商测量过程

1 先看协商测量的入口,下面代码位于上面的切入代码1处:

//这里有三个标记,mLayoutRequested上面已经分析过了,mStopped表示窗口是否停止,第三个参数暂时不用管
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
    final Resources res = mView.getContext().getResources();
    //...省略部分代码

    //进行协商测量,这里的入参依次是:DecorView,WidowManager.Layoutparams,resource,期望宽度,期望高度
    //返回值是window是否改变,上面也提到过,直接影响是否执行performMeasure()过程
    windowSizeMayChange |= measureHierarchy(host, lp, res,
            desiredWindowWidth, desiredWindowHeight);
}

2 measureHierarchy()流程,牛逼的思想

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;

    //这个标记表明:测量结果是否满足
    boolean goodMeasure = false;
    //只有在宽度是wrap_content的情况下才进行协商测量,原因看下面注释
    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        // On large screens, we don't want to allow dialogs to just
        // stretch to fill the entire width of the screen to display
        // one line of text. First try doing the layout at a smaller
        // size to see if it will fit.
        // 这段注释说明了为什么要进行协商测量
        // 大体意思就是:在大屏幕上展示一个对话框的时候,不想让对话框的宽度填满屏幕,尝试给一个较小的尺寸来展示,这样美观些

        final DisplayMetrics packageMetrics = res.getDisplayMetrics();
        //获取系统内置的默认尺寸,这个320dp
        res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
        int baseSize = 0;
        //进行单位转换,并把这个尺寸保存在baseSize
        if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
            baseSize = (int) mTmpValue.getDimension(packageMetrics);
        }
        // 如果baseSize==0,则不存在协商的条件,直接跳过
        // 如果desiredWindowWidth<baseSize,则不需要协商,也直接跳过
        if (baseSize != 0 && desiredWindowWidth > baseSize) {
            //这里将MeasureSize拼接为MeasureSpec,也就是在高两位加上了测量模式(下面有代码展示)
            childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            
            //进行一次measure,我们之前说过,measure之后会保存一些标记(这里加个标记: measure标记1!!!)
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            //measure之后来看是否满足
            if ((host.getMeasuredWidthAndState() & View.MEASURED_STATE_TOO_SMALL) == 0) {
                //没有这个标记,说明不比期望尺寸小,说明满足期望尺寸
                goodMeasure = true;
            } else {
                //不满足,继续搞,需要大一点,那么就取默认尺寸和期望尺寸的平均数(相加除以2)
                baseSize = (baseSize + desiredWindowWidth) / 2;
                //再组合一下尺寸,这里只搞了宽度,高度已经不需要了
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                //在measure一次
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                //再判断一下是否满足
                if ((host.getMeasuredWidthAndState() & View.MEASURED_STATE_TOO_SMALL) == 0) {
                    if (DEBUG_DIALOG) Log.v(mTag, "Good!");
                    //满足测量
                    goodMeasure = true;
                }//这里没有else,因为即使不满足,也没法处理了,不能再大了,干脆直接跳过,用期望尺寸去搞
            }
        }
    }

    //协商后还不满足 或者 根本就没参加协商过程(期望尺寸直接小于默认尺寸,goodMeasure还是false的)
    if (!goodMeasure) {
        //直接取期望值放进去
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        //measure一下
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        //检测尺寸是否变化
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
            //如果尺寸变化了,则更新这个标记
            windowSizeMayChange = true;
        }
    }
    //返回
    return windowSizeMayChange;
}

小结:

  • 1 协商测量的条件:只有wrap_content才执行,因为match_parent和具体值都是exactly,都是具体值,肯定是用户指定的,不能改变用户意图,所以只有wrap_content才使用
  • 2 协商测量的目的:使得dialog在大屏幕上显示美观
  • 3 协商测量的过程:
      1. 首先看期望值是否大于默认值,大于才进行协商,小于则不需要协商,直接使用期望值;
      1. 协商时先尝试使用系统默认的较小尺寸(320dp)来看是否满足,满足则使用;否则取期望值和默认值的和的一半(肯定大于320dp)来看是否满足,满足则使用,不满足则直接使用期望值
  • 4 我们发现,协商测量可能多次执行performMeasure(),所以一个View在显示过程中被measure()多次不需要惊讶,原因就在于此

performMeasure() 正式测量过程

先来看入口

//...省略关于windowSizeMayChange的判断,满足这个条件是进入下面代码的条件之一,条件太jb复杂了,故省略
if (!mStopped || mReportNextDraw) {
    boolean focusChangedDueToTouchMode = ensureTouchModeLocally((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
    if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||updatedConfiguration) {

        //拼装MeasureSpec,这里直接取出窗口尺寸
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

        //执行测量
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

        //获取测量后的尺寸
        int width = host.getMeasuredWidth();
        int height = host.getMeasuredHeight();
        boolean measureAgain = false;

        //是否设置权重,设置了的话,就需要再次测量
        if (lp.horizontalWeight > 0.0f) {
            width += (int) ((mWidth - width) * lp.horizontalWeight);
            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);
            measureAgain = true;
        }
        if (lp.verticalWeight > 0.0f) {
            height += (int) ((mHeight - height) * lp.verticalWeight);
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
            measureAgain = true;
        }

        //需要重新测量
        if (measureAgain) {
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        }

        //这里刷新了标记,需要重新布局
        layoutRequested = true;
    }
}

重点:performMeasure(int,int)

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        //调用了mView.measure(int,int);
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

View#Measure(int,int)

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

    //这一块表示是否打开了光学边界,对于开发者来说no egg use,不需要看的
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int oWidth  = insets.left + insets.right;
        int oHeight = insets.top  + insets.bottom;
        widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }

    //用一个long的高32位表示宽度,低32位表示高度,来缓存尺寸
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    //创建缓存
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

    //是否要强制更新布局,当你调用了View.requestLayout(),就会添加这个PFLAG_FORCE_LAYOUT标记
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

    //尺寸是否改变(注意:这里是MeasureSpec这个包含测量模式复合值,而不是MeasureSize这个只包含尺寸的数字)
    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec;
    //是否是精准模式(match_parent或精确尺寸,比如100dp)
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    //宽高是否改变,跟"尺寸是否改变"不同,这里只比较宽高,不比较测量模式
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec) && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
    //是否需要重新布局,这里使用了上面三个参数,条件是: 尺寸改变 并且 (需要一只测量 或者 不是精准模式 或者 宽高改变了)
    final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

    //如果强制布局 或者 需要重新布局
    if (forceLayout || needsLayout) {
        //去掉这个标记,这个标记表示已经正确的设置了尺寸,现在重新测量,肯定先去掉
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        //处理一下Rtl(从右往左排版)的情况
        resolveRtlPropertiesIfNeeded();

        //取出测量缓存,这个key就是上面用64位long存储宽高的那个玩意儿
        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        //若果没有缓存 或者 无视缓存
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            //直接调用onMeasure()
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            //去掉这个标记,这个标记表示,在布局之前是否需要触发一下onMeasure()
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            //有缓存,就取出缓存
            long value = mMeasureCache.valueAt(cacheIndex);
            //直接设置为宽高,宽为高32位,高为低32位,这里直接强转为int,正好只取低32位
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            //因为没有走onMeasure(),所以需要添加这个标记
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        //如果没有PFLAG_MEASURED_DIMENSION_SET这个标记,这里直接报错,这个标记是在setMeasuredDimension()里面添加的,就是检测有没有设置测量宽高的
        //所以重写onMeasure(),一定要调用这两个方法,除非有人xjb写也没法
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } //添加这个标记,表示需要重新布局 mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } //将本次测量的结果保存起来,作为下次的旧值来用 mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; //将测量结果缓存起来 mMeasureCache.put(key, ((long) mMeasuredWidth) << 32|(long) mMeasuredHeight & 0xffffffffL); } 

接下来看onMeasure(int,int)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //直接调用一行,但是参数有点深,下面我们只分析宽度,高度同理
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

//第一个参数是最小宽度,第二个是测量宽度
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    //获取测量模式
    int specMode = MeasureSpec.getMode(measureSpec);
    //获取测量尺寸
    int specSize = MeasureSpec.getSize(measureSpec);

    //如果是UNSPECIFIED,就返回最小宽度,否则返回测量宽度
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

protected int getSuggestedMinimumWidth() {
    //这个逻辑很简单,有背景就取背景和minWidth的最大值,没有背景就取minWidth
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

接下来看setMeasuredDimension(int,int)

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    //处理光学边界(跳过)
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }

    //直接看这个
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    //保存了宽高,现在可以通过getMeasuredWidth/Height()获取了
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    //添加了这个标记,还记得刚刚那个xjb写的crash吗,就是检测这个的
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

小结:

  • 1 performMeasure(int,int)直接调用了measure(int,int);
  • 2 measure(int,int)会先判断是否需要进行measure(int,int);如果尺寸变了或者手动调用了requestLayout()才需要进行measure(),measure()前会先清除PFLAG_MEASURED_DIMENSION_SET标记;
  • 3 如果测量值有缓存,则直接使用缓存值取进行设置宽高,同时设置PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT标记, 否则调用onMeasure(int,int);
  • 4 onMeasure(int,int)内部会调用setMeasureDimension(int,int),同时会根据是否有背景以及minWidth/minHeight的值来给出一个建议的大小;
  • 5 setMeasureDimension(int,int)最后还是调用了setMeasureDimension(int,int)来设置宽高,并且又添加了PFLAG_MEASURED_DIMENSION_SET这个标记,表示已经设置了宽高
  • 6 measure(int,int)末尾会对PFLAG_MEASURED_DIMENSION_SET这个标记进行检测,如果有人重写了onMeasure(int,int)但是没有调用setMeasureDimension(int,int)就不会又这个标记,就会报错
  • 7 measure(int,int)末尾还会把本次测量结果保存起来作为下次的旧值使用,并且还会缓存起来
  • 8 调用链: ViewRootImpl.performTraversals() -> ViewRootImpl.performMeasure() -> View.measure() -> View.onMeasure() -> View.setMeasureDimension() -> View.setMeasureDimensionRaw();

performLayout() 布局过程

先来看入口

//是否需要布局, layoutRequested在上一阶段末尾已经为true了,其他两个参数也分析过了
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout || mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
    //调用performLayout()
    performLayout(lp, mWidth, mHeight);
    
    //...省略其他代码
}

接下来看performLayout:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    
    //...省略其他代码

    final View host = mView;
    if (host == null) {
        return;
    }

    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
    try {
        //直接调用了host.layout();
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

        //...省略其他代码

    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

接下来看View#layout():

public void layout(int l, int t, int r, int b) {
    // 检测这个标记,如果有,就需要走一下onMeasure()
    // 还记得这个标记吗,在View的measure()里面,有缓存的时候是直接设置宽高并同时添加这个标记的,而没有调用onMeasure()
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        //onMeasure()后就去掉
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    //上一次的位置信息
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    // 通过setFrame()来设置左上右下,并返回是否需要改变(下面有代码,可以先看)
    // setOpticalFrame()是处理光学边界的,内部也调用了setFrame()
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    //如果位置改变了 或者 有PFLAG_LAYOUT_REQUIRED标记,还记得这个标记吗,在measure()后面添加的
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

        //触发onLayout()
        onLayout(changed, l, t, r, b);

        //...

        //去掉这个标记,表示此View已经布局过了
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        //回调onLayoutChange(),这里我们可以拿到我们的真实位置和mWidth/mHeight了
        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);
            }
        }
    }
}

来看下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;

        //...省略其他代码
    }
    return changed;
}

接着看View的onLayout:

//空实现
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

ViewGroup的onLayout:

//复写了此方法,声明为抽象的
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

小结:

  • 1 performLayout()直接调用View.layout()
  • 2 View.layout()会检测PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT这个标记来决定是否调用onMeasure()
  • 3 然后会先保存旧的位置,再通过setFrame()设置新位置,并清空PFLAG_LAYOUT_REQUIRED标记
  • 4 如果设置位置后发现有改变,就触发onLayout(),并回调onLayoutChange()
  • 5 调用链: ViewRootImpl.performTraversals() -> ViewRootImpl.performLayout() -> View.layout(){setFrame(l,t,r,b)}-> View.onLayout()

performDraw() 绘制过程

先看入口

// 条件很简单,预绘制 或者 可见
// 预绘制表示: 在绘制过程中又来了绘制请求,比如不断滑动
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
    //执行动画
    if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
        for (int i = 0; i < mPendingTransitions.size(); ++i) {
            mPendingTransitions.get(i).startChangingAnimations();
        }
        mPendingTransitions.clear();
    }

    //重点!
    performDraw();
} else {
    //不需要绘制,要么是在滑动,要么是不可见
    if (isViewVisible) {
        //这里表示预绘制,也就是在滑动,那么就不断触发performTraversals()从而不断进行布局绘制测量
        scheduleTraversals();
    } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
        //跑到这里就表示不可见,直接清除动画
        for (int i = 0; i < mPendingTransitions.size(); ++i) {
            mPendingTransitions.get(i).endChangingAnimations();
        }
        mPendingTransitions.clear();
    }
}

接着看performDraw():

private void performDraw() {
    //如果熄屏了,就跳过
    if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
        return;
    } else if (mView == null) {
        return;
    }

    //是否需要完全绘制,如果需要,就是绘制这个canvas,否则只绘制局部
    final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw;
    mFullRedrawNeeded = false;

    mIsDrawing = true;
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");

    //...省略Async逻辑

    try {
        //执行绘制
        boolean canUseAsync = draw(fullRedrawNeeded);
        //..省略部分代码
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

    //...省略部分代码
}

上述代码省略了大量的Async逻辑,关于Async的逻辑可以看另一篇Handler源码分析之二 异步消息的处理 接着看draw():

private boolean draw(boolean fullRedrawNeeded) {
    Surface surface = mSurface;

    //检测surface
    if (!surface.isValid()) {
        return false;
    }

    //展示fps
    if (DEBUG_FPS) {
        trackFPS();
    }

    //...省略滑动逻辑

    //获取缩放比例
    final float appScale = mAttachInfo.mApplicationScale;
    //是否需要缩放
    final boolean scalingRequired = mAttachInfo.mScalingRequired;

    //绘制区域
    final Rect dirty = mDirty;
  
    //如果需要完整绘制,则直接将dirty全部清空
    if (fullRedrawNeeded) {
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    }

    //回调onDraw(),不是View的onDraw(),是Listener的
    mAttachInfo.mTreeObserver.dispatchOnDraw();

    //根据是否在进行动画计算垂直滚动距离
    boolean animating = mScroller != null && mScroller.computeScrollOffset();
    final int curScrollY;
    if (animating) {
        curScrollY = mScroller.getCurrY();
    } else {
        curScrollY = mScrollY;
    }

    //计算偏移量
    int xOffset = -mCanvasOffsetX;
    int yOffset = -mCanvasOffsetY + curScrollY;
    final WindowManager.LayoutParams params = mWindowAttributes;
    //如果有insets,需要减去
    final Rect surfaceInsets = params != null ? params.surfaceInsets : null;
    if (surfaceInsets != null) {
        xOffset -= surfaceInsets.left;
        yOffset -= surfaceInsets.top;

        dirty.offset(surfaceInsets.left, surfaceInsets.right);
    }

    //记录绘制时间
    mAttachInfo.mDrawingTime = mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;

    boolean useAsyncReport = false;
    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        //如果开启了硬件加速,则使用硬件加速
        if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
            //...省略部分代码
            useAsyncReport = true;
            //使用硬件绘制
            mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
        } else {
            //这里表示软件绘制

            //...省略部分代码

            //直接调用drawSoftware()
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                    scalingRequired, dirty, surfaceInsets)) {
                return false;
            }
        }
    }

    //...省略部分代码
    if (animating) {
        mFullRedrawNeeded = true;
        scheduleTraversals();
    }
    return useAsyncReport;
}

接下来直接看drawSoftware():

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
                                 boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
                                    
    //画布
    final Canvas canvas;

    //处理偏移量
    int dirtyXOffset = xoff;
    int dirtyYOffset = yoff;
    if (surfaceInsets != null) {
        dirtyXOffset += surfaceInsets.left;
        dirtyYOffset += surfaceInsets.top;
    }

    try {
        //先减去边距
        dirty.offset(-dirtyXOffset, -dirtyYOffset);
        //再获取画布
        canvas = mSurface.lockCanvas(dirty);

        canvas.setDensity(mDensity);
    } catch (Surface.OutOfResourcesException e) {
        handleOutOfResourcesException(e);
        return false;
    } catch (IllegalArgumentException e) {
        Log.e(mTag, "Could not lock surface", e);
        mLayoutRequested = true;
        return false;
    } finally {
        //最后再加上边距
        dirty.offset(dirtyXOffset, dirtyYOffset);
    }

    try {
        //如果canvas不是实心的,也就是带有透明度的,则需要清除,否则透明下的内容会被看到
        //如果有偏移,则也需要清除,否则在偏移区域外的内容会被看到
        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }

        //清空dirty
        dirty.setEmpty();
        mIsAnimating = false;
        //添加PFLAG_DRAWN标记,表示已经绘制
        mView.mPrivateFlags |= View.PFLAG_DRAWN;
        
        //根据偏移量将坐标系切换到View的坐标系
        canvas.translate(-xoff, -yoff);
        //调用View的draw()
        mView.draw(canvas);

        //无障碍功能逻辑
        drawAccessibilityFocusedDrawableIfNeeded(canvas);
    } finally {
        try {
            surface.unlockCanvasAndPost(canvas);
        } catch (IllegalArgumentException e) {
            mLayoutRequested = true;
            return false;
        }
    }
    return true;
}

这里面涉及比较多的坐标转换,如果不熟悉的话,可以看Android View基础 接着看View.draw():

public void draw(Canvas canvas) {

    //此时的坐标系已经处理过滑动偏移量了,此时是View内容的坐标系

    final int privateFlags = mPrivateFlags;
    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;

    //画背景
    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条件的
    if (!verticalEdges && !horizontalEdges) {

        // Step 3, draw the content
        onDraw(canvas); //回调onDraw()

        // Step 4, draw the children
        dispatchDraw(canvas); //dispatchDraw()

        drawAutofilledHighlight(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);

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }
}

这里的onDraw(canvas)我们就不看了,不同的view有不同的实现,我们来看下drawBackground(canvas):

private void drawBackground(Canvas canvas) {

    //背景就是个drawable
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }

    //设置背景边界,我们知道drawable如果不设置边界的话,绘制出来是没有效果的
    setBackgroundBounds();

    //...省略硬件加速逻辑


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

        //上述逻辑说明:View背景的绘制是无视滑动的,也解释了为什么在滑动的过程中,View的背景不动?
    }
}

//设置边界逻辑
void setBackgroundBounds() {
    if (mBackgroundSizeChanged && mBackground != null) {
        //直接设置为了当前view的大小
        mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
}

接着来看下dispatchDraw(canvas),直接看ViewGroup的即可。 ViewGroup#dispatchDraw(canvas):

protected void dispatchDraw(Canvas canvas) {
    //此时的坐标系已经处理过偏移量了,此时是View内容的坐标系

    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;

    //...省略动画逻辑代码

    int clipSaveCount = 0;

    //处理clipToPadding标签,如果为false,表示允许绘制到父布局的padding区域
    final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
    if (clipToPadding) {
        //如果clipToPadding=true,则先保存当前图层
        clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
        //然后变换canvas
        canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                mScrollX + mRight - mLeft - mPaddingRight,
                mScrollY + mBottom - mTop - mPaddingBottom);
    }

    // We will draw our child's animation, let's reset the flag
    mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
    mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;

    boolean more = false;
    final long drawingTime = getDrawingTime();

    //...

    //获取child的存储列表
    final ArrayList<View> preorderedList = usingRenderNodeProperties ? null : buildOrderedChildList();
    //是否是用户指定了child的顺序
    final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
    
    //遍历绘制
    for (int i = 0; i < childrenCount; i++) {
        //...

        //依次获取child
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);

        //如果child可见 或者 在执行动画,就绘制
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    
    //...省略部分代码

    //绘制完毕,还原canvas
    if (clipToPadding) {
        canvas.restoreToCount(clipSaveCount);
    }

    // mGroupFlags might have been updated by drawChild()
    flags = mGroupFlags;

    //...
}

接着看drawChild:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    //又掉了view.draw(),注意!!这里是三个参数的draw,跟上面的不一样
    return child.draw(canvas, this, drawingTime);
}

接下来看view.draw(canvas,viewGroup,drawingTime):

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    //此时的坐标系是父View的坐标系

    // 是否开启硬件加速,我们假设不开启,也就是false
    final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
    // 这个值是false
    boolean drawingWithRenderNode = mAttachInfo != null
            && mAttachInfo.mHardwareAccelerated
            && hardwareAcceleratedCanvas;

    boolean more = false;
    final int parentFlags = parent.mGroupFlags;

    Transformation transformToApply = null;
    //是否需要缩放
    final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;

    //...省略动画逻辑

    //标记为已经绘制
    mPrivateFlags |= PFLAG_DRAWN;

    //...

    //绘图缓存
    Bitmap cache = null;
    // 获取绘制方式
    int layerType = getLayerType();
    // 如果是软件绘制(这里直接假定是)
    if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
        if (layerType != LAYER_TYPE_NONE) {
            layerType = LAYER_TYPE_SOFTWARE;
            //构建缓存
            buildDrawingCache(true);
        }
        //获取缓存
        cache = getDrawingCache(true);
    }

    //滑动偏移量处理
    int sx = 0;
    int sy = 0;
    if (!drawingWithRenderNode) {
        computeScroll();
        sx = mScrollX;
        sy = mScrollY;
    }

    //是否使用缓存绘制
    final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
    //是否有偏移
    final boolean offsetForScroll = cache == null && !drawingWithRenderNode;

    //保存当前图层
    int restoreTo = -1;
    if (!drawingWithRenderNode || transformToApply != null) {
        restoreTo = canvas.save();
    }
    //计算偏移
    if (offsetForScroll) {
        canvas.translate(mLeft - sx, mTop - sy);
        //上面这一行表示将坐标系切换到自己内容区的坐标系,可以拆分为:
        //1 canvas.translate(mLeft,mTop): 将坐标系从父View的坐标系切换到自己的坐标系
        //2 canvas.translate(-sx,-sy): 将坐标系从自己的坐标系切换到自己内容区的坐标系

    } else {
        if (!drawingWithRenderNode) {
            //没有偏移,只需要切换到自己的坐标系(同时也是内容区的坐标系)即可
            canvas.translate(mLeft, mTop);
        }
        //缩放处理
        if (scalingRequired) {
            if (drawingWithRenderNode) {
                restoreTo = canvas.save();
            }
            final float scale = 1.0f / mAttachInfo.mApplicationScale;
            canvas.scale(scale, scale);
        }
    }

    float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
    
    //省略矩阵变换处理

    if (!drawingWithRenderNode) {
        //处理clipChildren标签
        if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
            if (offsetForScroll) {
                //如果有偏移,则需要加上偏移量
                canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
            } else {
                if (!scalingRequired || cache == null) {
                    //没有cache,裁剪整个区域
                    canvas.clipRect(0, 0, getWidth(), getHeight());
                } else {
                    //否则取cache
                    canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
                }
            }
        }

        if (mClipBounds != null) {
            canvas.clipRect(mClipBounds);
        }
    }

    //是否使用缓存绘制
    if (!drawingWithDrawingCache) {
        //不使用缓存
        if (drawingWithRenderNode) {
            //...
        } else {
            //如果有PFLAG_SKIP_DRAW标签,表示不需要绘制背景,直接dispatchDraw()即可,ViewGroup默认带有此标签
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchDraw(canvas);
            } else {
                //没有PFLAG_DIRTY_MASK,乖乖的走draw(canvas)
                draw(canvas);
            }
        }
    } else if (cache != null) {
        //使用缓存绘制,且有缓存
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
            //如果没有layerType,就取父View的
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            //直接绘制到Bitmap即可
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            int layerPaintAlpha = mLayerPaint.getAlpha();
            if (alpha < 1) {
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            }
            //直接绘制到Bitmap
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            if (alpha < 1) {
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
        }
    }

    //还原
    if (restoreTo >= 0) {
        canvas.restoreToCount(restoreTo);
    }

    //..省略硬件加速代码
    
    mRecreateDisplayList = false;

    return more;
}

小结:

  • 1 performDraw()直接调用了draw(boolean),在这里处理了动画,无障碍功能等,并根据是否开启硬件加速进行软硬绘制
  • 2 软件绘制部分计算了边距,滚动处理,并根据偏移量切换到View内容的坐标系,然后调用view.draw()
  • 3 在view.draw()内部,依次绘制背景,onDraw(),子元素(dispatchDraw()),前景等元素
  • 4 绘制背景时会先将坐标系切换到View的坐标系,然后绘制背景,完事再切换回View内容的坐标系
  • 5 dispatchDraw()内部回遍历调用drawChild(),drawChild()内部又调用了三个参数的view.draw(canvas,viewGroup,time)
  • 6 三个参数的draw内部,又做了矩阵变换,坐标系变换,动画处理,绘图缓存等,最后又调用个一个参数的draw(),如此循环,知道全部绘制完毕.
  • 7 调用链:ViewRootImpl.performTraversals() -> ViewRootImpl.performDraw() -> ViewRootImpl.draw(canvas) -> ViewRootImpl.drawSoftware() -> View.draw(){onDraw()} -> ViewGroup.dispatchDraw() -> ViewGroup.drawChild() -> View.draw(canvas,viewGroup,time) -> view.draw(canvas) -> …

总结

View的整体测量、布局、绘制流程大概就完事了,可以把View理解为一个多叉树,每次测量布局绘制都是从树根开始向下分发,是个深度遍历的过程。而每次View有更新,都会自底向上回溯到根节点,并且沿路添加标记,然后再从根节点向下回溯,检测标记来更新并清除标记。

今天的文章View的测量布局绘制过程分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/14115.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注