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

更多相关文章

  1. android scrollview嵌套listview出现高度显示不全解决方案
  2. Android(安卓)动态设置控件高度
  3. android CursorAdapter
  4. android圆角dialog,并限制宽度和高度
  5. Android常用代码
  6. Android(安卓)fragment中如何对listview添加监听事件
  7. Android错误集
  8. Android(安卓)通知之 Notification
  9. android MVVM ItemViewSelector 实现列表中不同的view类型

随机推荐

  1. Android Audio and Video
  2. Android—锁定横屏遇到的问题
  3. Android 设置没有 actionBar的 样式
  4. android ListView 属性
  5. Android发送短信方法实例详解
  6. 安卓9.0 http请求数据失败解决办法
  7. [置顶] android orm映射框架(类似hibernat
  8. Android(安卓)MVVM理解以及运用
  9. 旅行青蛙(旅かえる)逆向笔记
  10. android 单选框