Android进阶学习(四)View的工作原理

文本是阅读《Android开发艺术探索》的学习笔记记录,详细内容可以自行阅读书本。

View的工作原理

1 初识ViewRoot和DecorView

  ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
  View的绘制过程是从ViewRoot的performTraversals方法进行的,它进过三个过程measure、layout、draw最终将一个View绘制出来。其中measure用来测量View的宽和高,layout用来确定View在父容器中的放置位置,而draw则负责绘制到屏幕上。

在这里插入图片描述
  如上图所示,performTraversals会依次调用performMeasure、performLayout和performDraw三个方法。这三个方法分别完成顶级View的measure、layout和draw这三大流程。

  其中performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure就从父容器传递到了子元素中了,这样就完成一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View树的遍历。同理,performLayout和performDraw的传递流程和performMeasure是类似的,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的。

  measure过程决定了View的宽/高,可以通过getMeasuredHeight()和getMeasuredWidth()方法获取测试后的宽/高。在几乎所有情况下它都等同于View最终的宽/高。
  Layout过程决定了View的四个顶点的坐标和实际的View的宽/高,完成后,可以通过getTop()、getBottom()、getLeft()和getRight()来获取View的四个顶点位置,并可以通过getWidth和getHeight方法来拿到View的最终宽/高。
  Draw过程则决定了View的显示,只有draw方法完成后,View的内容才能呈现在屏幕上。

2 理解MeasureSpec

  为了更好理解View的测量过程,我们还需要理解MeasureSpec。它很大程度上决定了一个View的尺寸规格。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,再根据这个MeasureSpec来测量出View的宽/高。

2.1 MeasureSpec

  MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize。
  SpecMode表示测量模式。SpecSize表示规格大小。部分源码如下:

 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);
        }
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

  可以看出,MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,也为其提供了打包和解包方法。
  SpecMode有三类,如下所示。

SpecMode含义
UNSPECIFIED父容器没有施加任何限制,可以是任何它想要的大小。
EXACTLY父容器已经检测出了View所需要的精确大小,这个时候View的最终大小接受SpecSize指定的值。它对应LayoutParams中的match_parent和具体数值两种模式。
AT_MOST父容器指定了一个可用大小,View的大小不能大于这个值,具体要看不同View的具体实现。它对应LayoutParams中的wrap_content。

2.2 MeasureSpec和LayoutParams对应关系

  MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才可以决定View的MeasureSpec,从而进一步决定View的宽/高。另外,对应顶层View(即DecorView)和普通View来说,MeasureSpec的转换过程略有不同。
  对于DecorView,其MeasureSpec是由窗口的尺寸和其自身的LayoutParams来共同确定;
  对于普通View,其MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams来共同决定。

  DecorView,在ViewRootImpl中的measureHierarchy方法中有如下一段代码:

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

  它展示了DecorView的MeasureSpec的创建过程,其中baseSize和desiredWindowHeight是屏幕尺寸。

  接着再看一下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;
    }

  通过上述代码就可以看出DecorView的MeasureSpec的产生过程,具体来说遵守如下规则,根据它的LayoutParams中的宽/高的参数来划分。
  LayoutParams.MATCH_PARENT:精确模式,大小就是窗口大小。
  LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口大小。
  固定大小:精确模式,大小为LayoutParams中指定的大小。

  对于普通View来说,这里指我们布局中的View,View的measure过程由ViewGroup传递而来,先看一下ViewGroup的measureChildWithMargins方法:

    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,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到MeasureSpec。从代码可以得知,子元素的MeasureSpec创建与父容器的MeasureSpec和子元素的LayoutParams还有View的margin及padding有关。具体内容可以看ViewGroup的getChildMeasureSpec代码:

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

  从上述代码可以理解,主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec。它遵循如下规律:

在这里插入图片描述
  从上表分析结果看。
  当View采用固定宽/高时,无论父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式,且大小遵循LayoutParams的大小。
  当View的宽/高是match_parent时,如果父容器模式是精确模式,则View也是精确模式且大小是父容器剩余空间。如果父容器模式是最大模式,则View是最大模式且大小不超过父容器的剩余空间。
  当View的宽/高是wrap_content时,不管父容器,View总是最大模式且大小不能超过父容器剩余空间。

3. View的工作流程

  View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制。

3.1 measure过程

  measure过程分情况来看,如果只是一个原始View,那么通过measure方法就完成了测量过程。如果是一个ViewGroup,除了完成直接的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。

3.1.1 View的measure

  View的measure过程由measure方法来完成,该方法是个final类型方法,意味着不能重写此方法,在View的measure方法中会调用View的onMeasure方法,onMeasure方法如下:

    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情况,getDefaultSize方法返回的就是measureSpec的specSize,即View测量后的大小。UNSPECIFIED情况,一般是系统内部的测量过程,暂不分析。

  由此得出结论,View的宽/高由specSize决定,直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。具体原因可以结合之前分析的规则表格。

  我们也可以在代码中重写onMeasure方法来解决这个问题:

    @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(100, 100);
        } else if (widthSpecMode == MeasureSpec.AT_MOST ) {
            setMeasuredDimension(100, heightSpecSize);
        } else {
            setMeasuredDimension(widthSpecSize, 100);
        }
    }
3.1.2 ViewGroup的measure

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

    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方法:

    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和父容器的MeasureSpec来创建属于子元素的MeasureSpec。getChildMeasureSpec方法上个小节分析了就不说了。

  为什么ViewGroup不像View一样对其onMeasure方法做统一实现?因为不同ViewGroup子类有不同的布局特性,这导致它们的测量细节各不一样,无法统一实现。

  通过LinearLayout的onMeasure方法来分析过程:

  首先来看LinearLayout的onMeasure方法代码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

  上述代码区分布局方向来实现,我们查看一个竖直布局的测量过程分析,部分代码如下:

for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
. . .
                // 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));
. . .

  上述代码可以看出,系统会遍历子元素并对每个子元素执行measureChildBeforeLayout方法,这个方法调用子元素的measure方法,这样每个子元素就开始进入measure过程,并且系统会通过mTotalLength存储LinearLayout在竖直方向的初步高度。

  针对竖直的LinearLayout,它在水平方向测量过程遵循View的测量过程,在竖直方向的测量过程与View不同。具体来说,当它高采用match_parent,则测量过程和View一致。如果高采用wrap_content,那么它的高度是所有子元素所占高度总和,但是仍不超过它的父容器的剩余空间,该过程源码如下:

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

3.2 layout过程

  Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup位置被确认后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onL哎呦他方法会被调用。

  先来看View的layout方法,b部分源码如下:

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

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

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

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

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

        final boolean wasLayoutValid = isLayoutValid();

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
. . . 

  上述代码可以看出,layout方法大致流程,首先通过setFrame方法来设定View的四个顶点,View的四个顶点确定了,那么View在父容器中的位置也就确定了;接着调用onLayout方法,这个方法是父容器确定子元素的位置。因为onLayout的具体实现同样和具体的布局有关,所有View和ViewGroup均没有真正实现onLayout方法。

3.3 draw过程

  View的绘制过程遵循如下步骤:
  1)绘制背景(background.draw(canvas))
  2)绘制自己(onDraw)
  3)绘制children(dispatchDraw)
  4)绘制装饰(onDrawScrollBars)
可以通过源码看出:

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        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;

        if (!dirtyOpaque) {
            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 (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

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

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

  View的绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历所有子元素的draw方法,如此draw事件就一层层传递下去.

4.4 自定义View

4.4.1 自定义View的分类

  1. 继承View重写onDraw方法

  这种方法主要用于实现一些不规则效果,即无法通过布局组合方式来达到。采用这种绘制方式实现,需要重写onDraw方法。还需要自己支持wrap_content,并且padding也需要自己处理。

  1. 继承ViewGroup派生特殊的Layout

  这种方法主要用于实现自定义布局,重定义一种新布局。采用此方式复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,同时处理子元素的测量和布局过程。

  1. 继承特定的View(比如TextView)

  一般是用于扩展某种已有View的功能,比如TextView,这种方法比较容易实现。

  1. 继承特定的ViewGroup(比如LinearLayout)

  当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。

4.2 自定义View须知

  1. 让View支持wrap_content

  2. 尽量不在View中使用Handler,没必要

  View本身已经提供了post序列方法

  1. View中如果有线程或者动画,需要及时停止,参考onDetachedFromWindow方法

  2. View中有滑动嵌套时,需要处理滑动冲突问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值