关于android:UI绘制流程之测量流程

39次阅读

共计 11863 个字符,预计需要花费 30 分钟才能阅读完成。

前言

上一篇咱们讲到最根本的 android 运行流程和绘制流程的调用程序,那么咱们最终失去的一个论断是 activity 的生命周期是有零碎服务所触发, 由零碎服务发动 handle 调用到 handleResumeActivity() 开始绘制流程而后最终交由 ViewRootImpl 调用到 performTraversals()
而后顺次之行了咱们 UI 的理论绘制流程 measure(测量),layout(布局摆放),Draw(具体绘制)
那么明天咱们须要理解的是对 UI 具体的绘制流程 measure,layout,Draw 进行深入分析,并且依靠于明天所学内容实现自定义瀑布流式布局!

1.View 的测量

首先咱们找到了绘制流程当中 performTraversals()的测量布局办法

if (!mStopped || mReportNextDraw) {
            boolean focusChangedDueToTouchMode = ensureTouchModeLocally((relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                    || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                    updatedConfiguration) {int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed!  mWidth="
                        + mWidth + "measuredWidth=" + host.getMeasuredWidth()
                        + "mHeight=" + mHeight
                        + "measuredHeight=" + host.getMeasuredHeight()
                        + "coveredInsetsChanged=" + contentInsetsChanged);

                 // Ask host how big it wants to be
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                // Implementation of weights from WindowManager.LayoutParams
                // We just grow the dimensions as needed and re-measure if
                // needs be
                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) {if (DEBUG_LAYOUT) Log.v(mTag,
                            "And hey let's measure once more: width=" + width
                            + "height=" + height);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                }

                layoutRequested = true;
            }
        }

performMeasure 办法,在这里他与调用到的

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

在这里咱们能够看到以后他调用了 view 当中的测量, 那么这里我门次要是对于测量方法当中的代码进行剖析, 在调用 performMeasure 调用之前须要两个参数

            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
             performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

他这里的意思是,传入宽高的测量规格,那么这个规格是什么意思?那么进入到 getRootMeasureSpec 办法时我门会看到

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

而后在这里我门看到了一个对象 MeasureSpec,MeasureSpec 的作用是在在 Measure 流程中,零碎将 View 的 LayoutParams 依据父容器所施加的规定转换成对应的 MeasureSpec(规格),而后在 onMeasure 中依据这个 MeasureSpec 来确定 view 的测量宽高。这是咱们关上 MeasureSpec 源码, 在这两头咱们会看到上面这几个办法

   public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT; 
     /**
      * UNSPECIFIED 模式:* 父 View 不对子 View 有任何限度,子 View 须要多大就多大
      */ 
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
      * EXACTYLY 模式:* 父 View 曾经测量出子 Viwe 所须要的准确大小,这时候 View 的最终大小
      * 就是 SpecSize 所指定的值。对应于 match_parent 和准确数值这两种模式
      */ 
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
      * AT_MOST 模式:* 子 View 的最终大小是父 View 指定的 SpecSize 值,并且子 View 的大小不能大于这个值,* 即对应 wrap_content 这种模式
      */ 
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    // 将 size 和 mode 打包成一个 32 位的 int 型数值
    // 高 2 位示意 SpecMode,测量模式,低 30 位示意 SpecSize,某种测量模式下的规格大小
    public static int makeMeasureSpec(int size, int mode) {if (sUseBrokenMakeMeasureSpec) {return size + mode;} else {return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    // 将 32 位的 MeasureSpec 解包,返回 SpecMode, 测量模式
    public static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);
    }

    // 将 32 位的 MeasureSpec 解包,返回 SpecSize,某种测量模式下的规格大小
    public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);
    }
    //...
}

测量模式
    EXACTLY:父容器曾经测量出所须要的准确大小,这也是 childview 的最终大小
            ------match_parent,准确值是爸爸的

    ATMOST : child view 最终的大小不能超过父容器的给的
            ------wrap_content 准确值不超过爸爸

    UNSPECIFIED: 不确定,源码外部应用
            ------- 个别在 ScrollView,ListView 

那么在此处咱们能够看到,在出咱们测量方法当中他传递的并不是一个纯正的 size,而是遵循了咱们设置宽高时的几种模式:EXACTLY ,ATMOST ,UNSPECIFIED这三个模式的实质是 0,1,2 的左位移 30 位

那么其实咱们先能了解为咱们实际上在传递值得过程当中将显示模式 +size 打包一起交给 measure 办法,而外面的数据结构其实实际上是一个 32 位的数值,咱们能够显著看到几个模式的值在前面进行了左位移操作了 30 位,用 MODE_SHIFT 操作之后,理论表明一个 32 位的值 30 前两位作为 MODE,而MODE_MASK 示意是后 30 位,所以当初能得出一个论断他们的数据构能够看成是

而这个时候咱们看到有三个办法负责打包解析

  • makeMeasureSpec– 负责打包 mode 和 size
  • getMode– 负责解析失去 mode 局部
  • getSize– 负责解析失去 size 局部

那么打包时使用了 (size & ~MODE_MASK) | (mode & MODE_MASK) 的算法进行混合, 那么这里咱们能够认为 是 size 转化成 32 位后放入后 30 位 组合 mode(mode 放入前两位)

getMode 解析用了 measureSpec & MODE_MASK(解析只有前 2 位)

getSize 解析用了 measureSpec & ~MODE_MASK(不要前两位)

那么此处就能够失去,在测量时他真正的给我传递的是规格,而所谓的规格只不过是显示模式 + 理论宽高的一个数据包,或者你了解为这个值当中蕴含模式和具体数值就行了

所以咱们得出一个论断,View 的测量流程中,通过 makeMeasureSpec 来保留宽高信息,在其余流程通过 getMode 或 getSize 失去模式和宽高。那么问题来了,下面提到 MeasureSpec 是子容器和父容器的模式所独特影响的,那么,对于 DecorView 来说,它曾经是顶层 view 了,没有父容器,那么它的 MeasureSpec 怎么来的呢?是否还记得咱们的 setContent 所加载的零碎布局?

咱们的初始就曾经给了一个布局去装载,所以,在这里,他的爸爸是零碎布局

那么到目前为止,就曾经取得了一份 DecorViewMeasureSpec,它代表着根 View 的规格、尺寸,在接下来的 measure 流程中,就是依据已取得的根 View 的 MeasureSpec 来逐层测量各个子 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);
    }
}

这里很显著他间接调用搞得是 view 的 measure 这里的 mView 就是 DecorView,也就是说,从顶级 View 开始了测量流程

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
    ...
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {
        ...
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } 
    ...
 }

能够看到,它在外部调用了 onMeasure 办法所以这里开始调用 onMeasure, 那么留神,到此为止,咱们的布局容器的外围就在这里了,不论是LinearLayout 或者是 FreamLayout 还是其余布局,他们都是通过测量组件,实现咱们的布局定位,每一个 LayoutonMeasure实现都不一样,这里因为顶层是一个 FreamLayout 所以咱们

  参照 FreamLayout 作为案例
 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取以后布局内的子 View 数量
int count = getChildCount();

// 判断以后布局的宽高是否是 match_parent 模式或者指定一个准确的大小,如果是则置 measureMatchParent 为 false.
final boolean measureMatchParentChildren =
        MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
        MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();

int maxHeight = 0;
int maxWidth = 0;
int childState = 0;

// 遍历所有类型不为 GONE 的子 View
for (int i = 0; i < count; i++) {final View child = getChildAt(i);
    if (mMeasureAllChildren || child.getVisibility() != GONE) {
        // 对每一个子 View 进行测量
        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        // 寻找子 View 中宽高的最大者,因为如果 FrameLayout 是 wrap_content 属性
        // 那么它的大小取决于子 View 中的最大者
        maxWidth = Math.max(maxWidth,
                child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
        maxHeight = Math.max(maxHeight,
                child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        childState = combineMeasuredStates(childState, child.getMeasuredState());
        // 如果 FrameLayout 是 wrap_content 模式,那么往 mMatchParentChildren 中增加
        // 宽或者高为 match_parent 的子 View,因为该子 View 的最终测量大小会受到 FrameLayout 的最终测量大小影响
        if (measureMatchParentChildren) {
            if (lp.width == LayoutParams.MATCH_PARENT ||
                    lp.height == LayoutParams.MATCH_PARENT) {mMatchParentChildren.add(child);
            }
        }
    }
}

// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
    maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}

// 保留测量后果
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        resolveSizeAndState(maxHeight, heightMeasureSpec,
                childState << MEASURED_HEIGHT_STATE_SHIFT));

// 子 View 中设置为 match_parent 的个数
count = mMatchParentChildren.size();
// 只有 FrameLayout 的模式为 wrap_content 的时候才会执行下列语句
if (count > 1) {for (int i = 0; i < count; i++) {final View child = mMatchParentChildren.get(i);
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        // 对 FrameLayout 的宽度规格设置,因为这会影响子 View 的测量
        final int childWidthMeasureSpec;

        /**
          * 如果子 View 的宽度是 match_parent 属性,那么对以后 FrameLayout 的 MeasureSpec 批改:* 把 widthMeasureSpec 的宽度规格批改为: 总宽度 - padding - margin,这样做的意思是:* 对于子 Viw 来说,如果要 match_parent,那么它能够笼罩的范畴是 FrameLayout 的测量宽度
          * 减去 padding 和 margin 后剩下的空间。*
          * 以下两点的论断,能够查看 getChildMeasureSpec()办法:*
          * 如果子 View 的宽度是一个确定的值,比方 50dp,那么 FrameLayout 的 widthMeasureSpec 的宽度规格批改为:* SpecSize 为子 View 的宽度,即 50dp,SpecMode 为 EXACTLY 模式
          * 
          * 如果子 View 的宽度是 wrap_content 属性,那么 FrameLayout 的 widthMeasureSpec 的宽度规格批改为:* SpecSize 为子 View 的宽度减去 padding 减去 margin,SpecMode 为 AT_MOST 模式
          */
        if (lp.width == LayoutParams.MATCH_PARENT) {final int width = Math.max(0, getMeasuredWidth()
                    - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                    - lp.leftMargin - lp.rightMargin);
            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        } else {
            childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                    getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                    lp.leftMargin + lp.rightMargin,
                    lp.width);
        }
        // 同理对高度进行雷同的解决,这里省略...

        // 对于这部分的子 View 须要从新进行 measure 过程
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}

总结一下:

  • 首先,FrameLayout 依据它的 MeasureSpec 来对每一个子 View 进行测量,即调用 measureChildWithMargin 办法,这个办法上面会具体阐明;
  • 对于每一个测量实现的子 View,会寻找其中最大的宽高,那么 FrameLayout 的测量宽高会受到这个子 View 的最大宽高的影响(wrap_content 模式),接着调用 setMeasureDimension 办法,把 FrameLayout 的测量宽高保留。
  • 最初则是非凡状况的解决,即当 FrameLayout 为 wrap_content 属性时,如果其子 View 是 match_parent 属性的话,则要从新设置 FrameLayout 的测量规格,而后从新对该局部 View 测量。

在下面提到 setMeasureDimension 办法,该办法用于保留测量后果,在下面的源码外面,该办法的参数接管的是 resolveSizeAndState 办法的返回值那么咱们间接看 View#resolveSizeAndState 办法:

 public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
    case MeasureSpec.AT_MOST:
        if (specSize < size) {result = specSize | MEASURED_STATE_TOO_SMALL;} else {result = size;}
        break;
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    case MeasureSpec.UNSPECIFIED:
    default:
        result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
 }

能够看到该办法的思路是相当清晰的,当 specMode 是 EXACTLY 时,那么间接返回 MeasureSpec 外面的宽高规格,作为最终的测量宽高;当 specMode 时 AT_MOST 时,那么取 MeasureSpec 的宽高规格和 size 的最小值

2. 子 View 测量

下面在 FrameLayout 测量内提到的 measureChildWithMargin 办法,它接管的主要参数是子 View 以及父容器的 MeasureSpec,所以它的作用就是对子 View 进行测量,那么咱们间接看这个办法

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

外面调用了 getChildMeasureSpec 办法,把父容器的 MeasureSpec 以及本身的 layoutParams 属性传递进去来获取子 View 的 MeasureSpe,在这里咱们能够看到间接又调用了子类的 measure

总结

那么当初咱们能失去整体的测量流程:

在 performTraversals 开始取得 DecorView 种的零碎布局的尺寸,而后在 performMeasure 办法中开始测量流程,对于不同的 layout 布局有着不同的实现形式,但大体上是在 onMeasure 办法中,对每一个子 View 进行遍历,依据 ViewGroup 的 MeasureSpec 及子 View 的 layoutParams 来确定本身的测量宽高,而后最初依据所有子 View 的测量宽高信息再确定爸爸的宽高

一直的遍历子 View 的 measure 办法,依据 ViewGroup 的 MeasureSpec 及子 View 的 LayoutParams 来决定子 View 的 MeasureSpec,进一步获取子 View 的测量宽高,而后逐层返回,一直保留 ViewGroup 的测量宽高。

看完三件事❤️


如果你感觉这篇内容对你还蛮有帮忙,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的『点赞和评论』,才是我发明的能源。
  2. 关注公众号 『小新聊 Android』,不定期分享原创常识。
  3. 同时能够期待后续文章 ing🚀
  4. 关注后回复【666】扫码即可获取 Android 进阶学习材料包

正文完
 0