View 的工作流程(一)

measure 过程要分情况来看,如果只是一个原始的 View,那么通过 measure 方法就完成了测量过程,如果是一个 ViewGroup,除了完成自己的测量以外,还会遍历调用所有子元素的 measure 方法,各个子元素在递归去执行这个流程,下面针对两种情况分别分析。

1. View 的 measure 过程

View 的 measure 过程有由其 measure 方法来完成,measure 方法是一个 final 方法,所以子类是不能重写此方法的,在 View 的 measure 方法中会调用 View 的 onMeasure 方法,因此只需要看 onMeasure 方法即可,View 的 onMeasure 方法如下:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

上述代码简洁,但并不简单,setMeasuredDimension 方法设置了 View 的宽高的测量,来看下代码:

    /**
     * <p>This method must be called by {@link #onMeasure(int, int)} to store the
     * measured width and measured height. Failing to do so will trigger an
     * exception at measurement time.</p>
     *
     * @param measuredWidth The measured width of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     * @param measuredHeight The measured height of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     */
    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);
    }

可以看出,setMeasuredDimension 方法需要通过getDefaultSize 方法来得到 View 的宽高,然后在进行测量,因此我们只需要看 getDefaultSize 方法是如何处理的就可以:

    /**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    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;
    }

可以看出,getDefaultSize 方法的逻辑很简单,对于我们来说,我们只需要看 AT_MOST 和 EXACTLY这两种情况。简单的理解,其实 getDefaultSize 返回的大小就是 MeasureSpec 中的 specSize,而这个 specSize就是 View 测量后的宽高,这里多次提到测量后的大小,是因为 View 的最终大小是在 layout 阶段确定的,所以这里必须加以区分,但是几乎所有情况下 View 测量后的大小就是 View 的最终大小,需要注意的是不是所有情况,后续讲到 layout 过程的死后会说明,好了,我们继续往下看。

至于 UNSPECIFIEND 这种情况,一般用于系统内部的测量过程,在这种情况下,View 的大小为 getDefaultSize 的第一个参数 size,通过上边的源码我们可以看明白。而通过 onMeasure 的源码,我们可以看明白,这个 size 其实就是 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 这两个方法的返回值,我们来看下 getSuggestedMinimumWidth() 的源码,getSuggestedMinimumHeight() 和 getSuggestedMinimumWidth() 原理是一样的,所以只需要看其中一个就好:

    /**
     * Returns the suggested minimum height that the view should use. This
     * returns the maximum of the view's minimum height
     * and the background's minimum height
     * ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
     * <p>
     * When being used in {@link #onMeasure(int, int)}, the caller should still
     * ensure the returned height is within the requirements of the parent.
     *
     * @return The suggested minimum height of the view.
     */
    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }
从源码中,我们可以看出,如果 View 没有设置背景,那么我们得到的 View 的宽度就是 mMinWidth,而 mMinWidth 又对应于 android:minWidth 这个属性所指定的值,因此 View 的宽度即为 android:minWidth 所指定的值。如果 android:minWidth 不指定大小,那么它默认是0,也就是说,View 在 android:minWidth 不指定的情况下,宽高都是0,;如果 View 设置了背景,则 View 的宽度为 max(mMinWidth, mBackground.getMinimumWidth()),mMinWidth 的含义刚才我们已经说了,那么 mBackground.getMinimumWidth() 是什么呢?我们来看下 Drawable 的 getMinimumWidth() 方法(如果不明白为什么明明是 View 类,怎么又和 Drawable 类扯上了关系,请查看源码,这里就不解释了):
    /**
     * Returns the minimum width suggested by this Drawable. If a View uses this
     * Drawable as a background, it is suggested that the View use at least this
     * value for its width. (There will be some scenarios where this will not be
     * possible.) This value should INCLUDE any padding.
     *
     * @return The minimum width suggested by this Drawable. If this Drawable
     *         doesn't have a suggested minimum width, 0 is returned.
     */
    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

可以看出 getMinimumWidth() 返回的就是 Drawable 的原始宽度,前提是 Drawable 有原始宽度,否则返回0,那么 Drawable 在什么情况下有原始宽度呢?这里先举两个例子,以后会讲解具体的内容。

① ShapeDrawable 无原始的宽高;

② BitmapDrawable 欧原始的宽高。

 从 getDefaultSize 方法的实现来看,View 的宽高由 specSize 决定,所以我们可以得出结论,直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 warp_content 时的大小,否则在布局中使用 warp_content 就相当于使用 match_parent。为什么呢?这个原因结核上篇文章中的表格更好理解,如果 View 在布局中使用 warp_content,那么他的 specMode 是 AT_MOST,在这种模式下,他的宽高等于 specSize,查看表知道 View 的 specSize 是 parentSize,而 parentSize 是父容器中目前使用的大小,也就是当前父容器剩余空间的大小,很显然,View 的宽高就等于父容器当前剩余空间的大小,这种效果和在布局中使用 match_parent 完全一致,如何解决这个问题呢,方法如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if(widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if(heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }
在上面的代码中,我们只需要给 View 指定一个默认的内部宽高(mWidth 和 mHeight),并在 warp_content 的时候设置此宽高即可。对于非 warp_content  情形,我们沿用系统的测量值即可,至于这个默认的内部宽高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可,。如果查看 TextView、ImageView 等的源码就可以知道,针对 warp_content  情形,它们的 onMeasure 方法均做了特殊处,这里就不一一解读了,代码如下:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        BoringLayout.Metrics boring = UNKNOWN_BORING;
        BoringLayout.Metrics hintBoring = UNKNOWN_BORING;

        if (mTextDir == null) {
            mTextDir = getTextDirectionHeuristic();
        }

        int des = -1;
        boolean fromexisting = false;

        if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        } else {
            if (mLayout != null && mEllipsize == null) {
                des = desired(mLayout);
            }

            if (des < 0) {
                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
                if (boring != null) {
                    mBoring = boring;
                }
            } else {
                fromexisting = true;
            }

            if (boring == null || boring == UNKNOWN_BORING) {
                if (des < 0) {
                    des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
                }
                width = des;
            } else {
                width = boring.width;
            }

            final Drawables dr = mDrawables;
            if (dr != null) {
                width = Math.max(width, dr.mDrawableWidthTop);
                width = Math.max(width, dr.mDrawableWidthBottom);
            }

            if (mHint != null) {
                int hintDes = -1;
                int hintWidth;

                if (mHintLayout != null && mEllipsize == null) {
                    hintDes = desired(mHintLayout);
                }

                if (hintDes < 0) {
                    hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
                    if (hintBoring != null) {
                        mHintBoring = hintBoring;
                    }
                }

                if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
                    if (hintDes < 0) {
                        hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, mTextPaint));
                    }
                    hintWidth = hintDes;
                } else {
                    hintWidth = hintBoring.width;
                }

                if (hintWidth > width) {
                    width = hintWidth;
                }
            }

            width += getCompoundPaddingLeft() + getCompoundPaddingRight();

            if (mMaxWidthMode == EMS) {
                width = Math.min(width, mMaxWidth * getLineHeight());
            } else {
                width = Math.min(width, mMaxWidth);
            }

            if (mMinWidthMode == EMS) {
                width = Math.max(width, mMinWidth * getLineHeight());
            } else {
                width = Math.max(width, mMinWidth);
            }

            // Check against our minimum width
            width = Math.max(width, getSuggestedMinimumWidth());

            if (widthMode == MeasureSpec.AT_MOST) {
                width = Math.min(widthSize, width);
            }
        }

        int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
        int unpaddedWidth = want;

        if (mHorizontallyScrolling) want = VERY_WIDE;

        int hintWant = want;
        int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();

        if (mLayout == null) {
            makeNewLayout(want, hintWant, boring, hintBoring,
                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
        } else {
            final boolean layoutChanged = (mLayout.getWidth() != want) ||
                    (hintWidth != hintWant) ||
                    (mLayout.getEllipsizedWidth() !=
                            width - getCompoundPaddingLeft() - getCompoundPaddingRight());

            final boolean widthChanged = (mHint == null) &&
                    (mEllipsize == null) &&
                    (want > mLayout.getWidth()) &&
                    (mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));

            final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);

            if (layoutChanged || maximumChanged) {
                if (!maximumChanged && widthChanged) {
                    mLayout.increaseWidthTo(want);
                } else {
                    makeNewLayout(want, hintWant, boring, hintBoring,
                            width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
                }
            } else {
                // Nothing has changed
            }
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            height = heightSize;
            mDesiredHeightAtMeasure = -1;
        } else {
            int desired = getDesiredHeight();

            height = desired;
            mDesiredHeightAtMeasure = desired;

            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(desired, heightSize);
            }
        }

        int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
        if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
            unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
        }

        /*
         * We didn't let makeNewLayout() register to bring the cursor into view,
         * so do it here if there is any possibility that it is needed.
         */
        if (mMovement != null ||
            mLayout.getWidth() > unpaddedWidth ||
            mLayout.getHeight() > unpaddedHeight) {
            registerForPreDraw();
        } else {
            scrollTo(0, 0);
        }

        setMeasuredDimension(width, height);
    }

2. ViewGroup 的 measure 过程

对于 ViewGroup 来说,除了完成自己的 measure 过程以外,还会遍历去掉用所有子元素的 measure 方法,各个子元素在递归去执行这个过程。和 View 不同的是,ViewGroup 是一个抽象类,因此它没有重写 View 的 onMeasure 方法,但是它提供了一个 measureChildren 方法,代码如下:

    /**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
从上面代码来看,ViewGroup 在 measure 时,会对每一个子元素进行 measure,measureChild 这个方法实现也很好理解:
    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    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);
    }

很显然,measureChild 的思想就是取出子元素的 LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。

我们知道,ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类具体实现,比如 LinearLayout、RelativeLayout 等,为什么 ViewGroup 不像 View 一样对其 onMeasure 方法做统一实现呢?那是因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此,ViewGroup 不能做统一实现。下面通过 LinearLayout 的 onMeasure 方法来分析 ViewGroup 的 measure 过程,其他 Layout 类型读者可以自行分析。

首先来看 LinearLayout 的 onMeasure 方法,如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
上述代码很简单,我们来看下竖直布局的 LinearLayout 的测量过程,即 measureVertical 方法,measureVertical 的源码比较长,下面只描述大概意思,首先看一段代码:
     // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    // The heightMode is either UNSPECIFIED or AT_MOST, and
                    // this child is only laid out using excess space. Measure
                    // using WRAP_CONTENT so that we can find out the view's
                    // optimal height. We'll restore the original height of 0
                    // after measurement.
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    // Restore the original height and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }
从上面的代码可以看出,系统会遍历子元素并对每个子元素执行 measureChildBeforeLayout 方法,这个方法内部会调用子元素的 measure 方法,我们来看下:
    void measureChildBeforeLayout(View child, int childIndex,
                                  int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
                                  int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }
    
    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);
    }
通过调用这个方法,这样各个子元素开始依次进入 measure 过程,并且系统会通过 mTotalLength 这个变量来存储 LinearLayout 在竖直方向的初始高度,每次量一个子元素,mTotalLength 就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的 margins、padding等,当子元素测量完毕后,LinearLayout 会测量自己的大小,代码如下:
        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        ...
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
通过上述代码我们发现, 当子元素测量完毕之后,LinearLayout 会根据子元素的情况来测量自己的大小。针对竖直的 LinearLayout 而言,它在水平方向的测量过程遵循 View 的测量过程,在竖直方向的测量过程和 View 有所不同。具体来说是指,如果他的布局中高度采用的是 match_parent 或者具体的数值,那么他的测量过程和 View 一致,即高度为 specSize;如果它的布局中高度采用的是 warp_content,那么它的高度是所有子元素占用的高度的总和,但是仍然不能超过他的父容器的剩余空间,当然他的最终高度还需要考虑它在竖直方向的 padding,这个过程可以进一步看下源码:
    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);
    }

View 的 measure 过程是三大流程中最复杂的一个,measure 完成以后,通过 getMeasuredWidth/Height方法就可以正确获取到 View 的测量宽高,需要注意的是,在某些极端情况下,系统可能需要多次 measure 才能得到最终的宽高,在这种情形下,在 onMeasure 方法中拿到的测量宽高是不准确的,一个好的习惯是在 onLayout 方法中去获取 View 的测量宽高和最终宽高。

上面已经对 View 的 measure 过程进行了详细的分析,现在考虑一种情况。比如我们想在 Activity 已启动的时候就做一件任务,但是这一件任务需要获取某个 View 的宽高,读者可能会说,这很简单啊,在 onCreate 或者 onResume 里面获取这个 View 的宽高不就行了?读者可以自行试一下,实际上在 onCreate、onStart 和 onResume 中均无法正确获取到某个 View 的宽高信息,这是因为  Activity 的生命周期和 View 的 measure 过程不是同步执行的,因此无法保证 Activity 执行了 onCreate、onStart 和 onResume 时某个 View 已经测量完毕了,如果 View 还没有测量完毕,那获取到的宽高就是 0 了,那有没有办法解决这个问题呢?肯定是有的,下边来给出四种方法解决这个问题:

① Activity/View#onWindowFocusChanged

onWindowFocusChanged这个方法的含义是:View 已经初始化完毕了,宽高已经准备好了。这个时候去获取狂傲是没有问题的。需要注意的是,onWindowFocusChanged 会被调用多次,当 Activity 的窗口得到焦点和失去焦点的时候各调用一次,具体来说,Activity 继续执行和暂停执行的时候,onWindowFocusChanged  均会被调用,如果频繁的进行 onResume 和 onPause,那么 onWindowFocusChanged  会被频繁的调用,具体代码如下:

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        if(hasFocus) {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }

② view.post(runnable)

通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 lopper 调用此 runnable 的时候,View 已经初始化好了,具体代码如下:

    @Override
    protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }

③ ViewTreeObserver

使用 ViewTreeObserver 的众多回调可以完成这个功能,比如使用 OnGlobalLayoutListener 接口,当 View 树发生改变 或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法会被回调,因此这是获取 View 宽高一个焊好的时机,需要注意的是,伴随着 View 树的动态发生改变时,onGlobalLayout方法会被调用多次,具体代码如下:

        ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });

④ view.measure(int widthMeasureSpec, int heightMeasureSpec)

通过手动对 View 进行 measure 来得到 View 的宽高,这个方法比较复杂,这里要分情况处理,根据 View 的 LayoutParams来分:

a. match_parent

直接放弃,无法 measure 出具体的宽高,原因很简单,根据 View 的 measure 过程,构造此种 MeasureSpec 需要知道 parentSize,也就是父容器的剩余空间,而这个时候,我们无法知道 parentSize 的大小,所以理论上测不出 View 的大小。

b. 具体的数值(dp/px)

比如狂傲都是 100dp,如下measure:

        int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
        view.onMeasure(widthMeasureSpec, heightMeasureSpec);

c. warp_content

如下measure:

        int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
        view.onMeasure(widthMeasureSpec, heightMeasureSpec);
注意 (1 << 30) - 1,通过分析 MeasureSpec 的实现我们知道,View 的尺寸使用 30 位的二进制表示,也就是说最大 30 个 1(即 2 ^30 - 1),也就是 (1 << 30) - 1,在最大化的模式下,我们用 View 理论上支持的最大值去构造 MeasureSpec 是合理的。关于 View 的 measure,网络上有两个错误的用法,为什么说是错误的,首先是违背了系统的内部实现规范(因为无法通过错误的 MeasureSpec 去得出合法的 SpecMode,从而导致 measure 过程出错),其次不能保证一定能 measure 出结果。

第一种错误用法,不过这种用法已经被Google干掉了,makeMeasureSpec 的第一个参数必须是大于 0 的:

        int widthMeasureSpec = MeasureSpec.makeMeasureSpec( -1, MeasureSpec.UNSPECIFIED);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec( -1, MeasureSpec.UNSPECIFIED);
        view.onMeasure(widthMeasureSpec, heightMeasureSpec);

第二种错误用法:

       view.onMeasure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
到此,View 的 measure 过程就完成了。篇幅太长,只能把 Layout 和 Draw 过程放在下一篇文章来讲解了。


猜你喜欢

转载自blog.csdn.net/sinat_29874521/article/details/80065314