您的位置:首页 > 移动开发 > Android开发

从源码角度分析Android View的绘制机制(一)

2015-08-18 17:43 746 查看
在Android的学习道路上,每一个人员都免不了去翻阅Android的源码,因为只有从源码的角度分析问题,我们才能真正的玩转Android开发。最近由于工作比较闲,总想着想写点什么东西,正好自己也可以整理一下。考虑到view的显示机制是自定义view的基础,也是面试中经常被问到的问题,所以记录此文,和大家共享,因水平有限,望大家踊跃拍砖,不胜感激。

有过自定义view的同行们都应该知道,view的显示依托于activity的setContentView方法依附到PhoneWindow窗体上的,在显示的过程中,这个view会经历测量(measure)、布局(layout)、draw(绘制)三个阶段, measure阶段就是得到每个View的大小,layout阶段就是计算每个View在UI上的坐标,draw阶段就是根据前面两个阶段的数据进行UI绘制。绘制完毕后方可被显示在界面上。

measure:

当我们要讲一个xml显示到ui上时,就是把layout的id传入到activity的setContentView中去,最终调用的是ViewRoot的performTraversals方法,此方法担任着view的绘制工作。

private void performTraversals() {
        final View host = mView;
        if (DBG) {
            host.debug();
        }
        if (host == null || !mAdded)
            return;
        ......
         Rect frame = mWinFrame;
        if (mFirst) {
            fullRedrawNeeded = true;
            mLayoutRequested = true;
            DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics();
            desiredWindowWidth = packageMetrics.widthPixels;
            desiredWindowHeight = packageMetrics.heightPixels;
            ......
        }
        ........
        boolean insetsChanged = false;
        if (mLayoutRequested) {
            getRunQueue().executeActions(attachInfo.mHandler);
            if (mFirst) {
                host.fitSystemWindows(mAttachInfo.mContentInsets);
                mAttachInfo.mInTouchMode = !mAddedTouchMode;
                ensureTouchModeLocally(mAddedTouchMode);
            } else {
                if (!mAttachInfo.mContentInsets.equals(mPendingContentInsets)) {
                    mAttachInfo.mContentInsets.set(mPendingContentInsets);
                    host.fitSystemWindows(mAttachInfo.mContentInsets);
                    insetsChanged = true;
                }
                if (!mAttachInfo.mVisibleInsets.equals(mPendingVisibleInsets)) {
                    mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
                }
                if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                        || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                    windowResizesToFitContent = true;
                    DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics();
                    desiredWindowWidth = packageMetrics.widthPixels;
                    desiredWindowHeight = packageMetrics.heightPixels;
                }
            }

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

            host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...........
        }


在getRootMeasureSpec中传入的参数中有一个参数是lp的属性,而lp的定义是这样的:

final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
WindowManager.LayoutParams lp = mWindowAttributes;

public LayoutParams() {
      super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
      type = TYPE_APPLICATION;
      format = PixelFormat.OPAQUE;
}


所以也就意味着lp.width和lp.height的值都是match_parent。

在getRootMeasureSpec方法中,根据windowSize和rootDimension返回测量规格。拿lp.width打比方,也就是decorView的width。如果width的值是MATCH_PARENT,那么返回的测量规格就是windowSize + MeasureSpec.EXACTLY的值,如果是WRAP_CONTENT则是

windowSize + MeasureSpec.AT_MOST的值。

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


至于上述提到的为何将windowSize和rootDimension映射出来的mode值相加,我们需要了解另外一个知识点,先看下面的代码:

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(int size, int mode) {
            return size + mode;
        }
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        ..........
    }


MeasureSpec.makeMeasureSpec中只是简单的把size和mode进行相加然后返回。这里是因为这样的返回值可以表示出当前view的测量规格和测量大小,如AT_MOST、EXACTLY就是表示mode,他们都是占用2位并且都是int值,int值在java中是占用32位的,所以另外的30位在Android中被设计成了size。也就是说每个高2位表示specMode,而低30位表示尺寸的大小。

做完上一步操作后,就是开始调用host.measure操作来计算view的大小。而host代表的就是DecorView,decorView其实是个FrameLayout也就是个ViewGroup,但是在ViewGroup继承自View,而view的measure方法是不能被重写的,所以host.measure走的还是view的measure方法,在这个方法中调用了ViewGroup的 onMeasure(widthMeasureSpec, heightMeasureSpec)方法,所以我们移步到FrameLayout的onMeasure方法中

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int count = getChildCount();

        int maxHeight = 0;
        int maxWidth = 0;

        // Find rightmost and bottommost child
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
            }
        }

        // Account for padding too
        maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight;
        maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom;

        // 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(resolveSize(maxWidth, widthMeasureSpec),
                resolveSize(maxHeight, heightMeasureSpec));
    }


这个方法里面调用了measureChildWithMargins方法,其实ViewGroup的子类在测量的时候都会走这个方法,最后还是会调用View类的measure方法进行测量,原型如下:

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


在view的measure方法中的两个参数分别对应宽和高的measureSpec,该参数是父视图传递给子视图的一个整型值,也就是说是父视图提供给子视图的测量规格,因为子视图最终占用的窗体大小是由父视图和子视图共同决定的。因此childWidthMeasureSpec是父视图传递给子视图的一个建议值。在测量大小的时候,padding和margin的值也作为大小的一部分。

上面描述的仅仅是view的测量过程。下面分析一下LinearLayout的测量过程:

在LinearLayout的测量中以下为主要代码:

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

    /**
    *  如果是垂直方向的布局
    */
    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
        int maxWidth = 0;
        int alternativeMaxWidth = 0;
        int weightedMaxWidth = 0;
        boolean allFillParent = true;
        float totalWeight = 0;

        final int count = getVirtualChildCount();

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        boolean matchWidth = false;

        final int baselineChildIndex = mBaselineAlignedChildIndex;        
        final boolean useLargestChild = mUseLargestChild;

        int largestChildHeight = Integer.MIN_VALUE;

        // See how tall everyone is. Also remember max width.
        // 遍历所有的子View,获取所有子View的总高度,并对每个子View进行measure操作 
        // 并且记录高度最高的子view
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                // //如果child 是Null,则mTotalLength加0  
                mTotalLength += measureNullChild(i);
                continue;
            }
            if (child.getVisibility() == View.GONE) {
                ////如果child不可见,则跳过  
               i += getChildrenSkipCount(child, i);
               continue;
            }
            //拿到child的LayoutParams 
            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
            //将weight的值加到totalWeight,weight的值就是xml文件中的layout_weight属性的值  
            totalWeight += lp.weight;

            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                 /** 
                  如果父View的mode是EXACTLY,并且height==0 并且lp.weight>0
                  那么就先不measure这个child,直接把topMargin和bottoMargin等属性加到totaoLength中 
                */  
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            } else {
                int oldHeight = Integer.MIN_VALUE;
                //如果父View不是EXACLTY,并且lp.height == 0 && lp.weight > 0,那么将子View的height变为WRAP_CONTENT  
                if (lp.height == 0 && lp.weight > 0) {
                    oldHeight = 0;
                    lp.height = LayoutParams.WRAP_CONTENT;
                }
                measureChildBeforeLayout(
                       child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0);

                if (oldHeight != Integer.MIN_VALUE) {
                   lp.height = oldHeight;
                }

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

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }

            .....................

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {

                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

        if (useLargestChild && heightMode == MeasureSpec.AT_MOST) {
            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }

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

                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                        child.getLayoutParams();
                // Account for negative margins
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
        }

        // 给总高度加上自身的padding
        mTotalLength += mPaddingTop + mPaddingBottom;
        //将所有View的高度赋值给heightSize
        int heightSize = mTotalLength;

        // Check against our minimum height
        // 最小的高度就是背景的高度mBGDrawable.getMinimumHeight()
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
         //这里对heightSize再次赋值,不过如果LinearLayout是xml文件的根标签,并且设置到Activity的话  
        //此时heightSize的大小就是屏幕的高度,我们暂时就考虑等于屏幕高度的情况,其他情况类似
        // Reconcile our calculated size with the heightMeasureSpec
        // 如果heightMeasureSpec的size是精确的,那么这个heightsize值==heightMeasureSpec的size,也就是屏幕的高度
        heightSize = resolveSize(heightSize, heightMeasureSpec);

        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds
        //屏幕的高度还剩下delta
        int delta = heightSize - mTotalLength;
        if (delta != 0 && totalWeight > 0.0f) {
            //如果设置了weightsum属性,这weightSum等于weightsum的属性,否则等于totalWeight  
            float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

                if (child.getVisibility() == View.GONE) {
                    continue;
                }

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

                float childExtra = lp.weight;
                if (childExtra > 0) {
                    // Child said it could absorb extra space -- give him his share
                    // 计算方式:子view的weight属性值 * 剩余高度 / weight总和
                    int share = (int) (childExtra * delta / weightSum);
                    weightSum -= childExtra;
                    delta -= share;

                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight +
                                    lp.leftMargin + lp.rightMargin, lp.width);

                    // TODO: Use a field like lp.isMeasured to figure out if this
                    // child has been previously measured
                    if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
                        // child was measured once already above...
                        // base new measurement on stored values
                        // 重新设置子view的高度
                        int childHeight = child.getMeasuredHeight() + share;
                        if (childHeight < 0) {
                            childHeight = 0;
                        }

                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
                    } else {
                        // child was skipped in the loop above.
                        // Measure for this first time here      
                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
                                        MeasureSpec.EXACTLY));
                    }
                }

                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

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

            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            // TODO: Should we recompute the heightSpec based on the new total length?
        } else {
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                           weightedMaxWidth);
        }

        if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }

        maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        //所有的孩子View测量完毕,为自己设置大小
        setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize);

        if (matchWidth) {
            forceUniformWidth(count, heightMeasureSpec);
        }
    }


在测量的过程中,会遍历出所有的子view,计算出所有子View的总高度和totalWeight,并对每个子View进行measure操作 。

在遍历子view的child过程中,如果child 是Null,则mTotalLength加0 ,如果child是不可见的,不做任何操作继续遍历下一个子view,然后拿到child的layoutParams,取出child的layout_weight,然后 totalWeight加上weight值。

接着判断如果父视图提供的高度mode等于MeasureSpec.EXACTLY,并且child的height等于0,并且child的weight的值大于0的时候,则先不测量这个child,因为有了weight,在把所有的child测量完毕后,再根据weight的值分配高度,但是在这里还是要把child的margin值加上去的,因为就算weight怎么变,margin值是不影响的, mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin)。这里取max的原因是child的margin可能为负数。

如果不满足上一个条件mode等于MeasureSpec.EXACTLY,就需要测量child,经历过测量之后可以知道child的高度,然后把这个高度加入到mTotalLength 中。在这个条件里面,如果child的height等于0,并且child的weight的值大于0,会把当前view的height设置成LayoutParams.WRAP_CONTENT,然后去测量。

到目前为止只是测量出了总的高度,还没有按照weight分配高度,哈哈,上面说了这么多可能有读者快要晕了,没事,说完weight的分配,我会集合源码举个例子给大家讲述(见下一篇文章)。

紧接着给mTotalLength加上父视图的paddingTop和paddingBottom值,得出heightSize。但是每个view都是有背景的,背景是有个高度的,所以要比较heightSize和getSuggestedMinimumHeight()的值的大小,取最大值作为heightSize,getSuggestedMinimumHeight()返回的mBGDrawable.getMinimumHeight()也就是drawable背景的最小高度。接着通过resolveSize再次的给heightSize赋值,因为如果父视图的heightMeasureSpec的mode是精确的话,那么这个heightSize的最终值也就应该是父视图指定的大小。

接下来就开始按照weight分配高度了,在分配高度之前要保证余下的高度heightSize - mTotalLength 不能为0并且totalWeight要大于0。然后遍历子view,得到子view的weight,如果weight大于0,通过公式:child的weight属性值 * 剩余高度 / weight总和计算出child要从剩余的高度里面获得多少的高度值,然后刷新剩余的高度值,给child重新设置高度并进行测量。

最后所有的孩子View测量完毕,调用setMeasuredDimension为自己设置大小。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: