当前位置: 代码迷 >> Android >> 从源码视角分析Android View的绘制机制(一)
  详细解决方案

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

热度:37   发布时间:2016-04-27 23:24:36.0
从源码角度分析Android View的绘制机制(一)

在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为自己设置大小。

版权声明:本文为博主原创文章,未经博主允许不得转载(联系方式:QQ312037487 邮箱:[email protected])。

  相关解决方案