自定义控件,从LinearLayout源码谈起

(1)前言

android的进阶之路上,总少不了使用自定义控件。自定义控件按照不同的分法,有不同的分类,这里主要分为四类并在后面跟上例子:
1 继承自view,重写 onDraw方法;比如系统的TextView,ImageView
2 继承自ViewGroup,实现自己的自定义控件;卡片布局CascadLayout
3 继承自特定的view(比如ImageView),
圆角图片CircleImageView,自带清除按钮的EditText
4 继承自特定的ViewGroup,(比如LinearLayout,ListView)自定义控件-下拉刷新和上拉加载的listView

这里按照2继承自ViewGroup,实现自己的自定义控件为切入点,从LinearLayout继承ViewGroup源码角度分析自定义控件。(哈哈,这里偷换概念,把系统的LinearLayout当做自定义控件,主要是一个学习目的。)。自定义ViewGroup的一个简单项目,请参考我的文章,卡片布局CascadLayout

view的工作流程是measure,layout和draw三大流程,也就是测量,布局和绘制,通过这三大步骤来完成这个view的布局以及显示。下面也按照这个思路来一步步解析。

(2)源码解析之构造函数:

自定义控件,从LinearLayout源码谈起_第1张图片

自定义控件,从LinearLayout源码谈起_第2张图片
自定义控件,从LinearLayout源码谈起_第3张图片
这也是我们自定义viewGroup的第一步,完成自定义属性,然后在自己的构造函数中获取属性并完成初始化。

注意:
1 在values目录下面创建attrs.xml,当然也可以选择attrs_custom_viewgroup.xml这种按照attr_开头的形式进行定义。
2 继承viewGroup至少需要覆写一个构造函数

3 源码解析之measure过程:

在自定义ViewGroup中我们会选择重写onMeasure来完成测量过程。来一步一步分析一下。

3.1View 类中的mesure流程

首先在View类中有measure 方法中调用了onMeasure 方法,将measureSpec传递给onMeasure 方法。并且调用了我们每次onMeasure覆写之后都必须调用的方法setMeasuredDimension。
自定义控件,从LinearLayout源码谈起_第4张图片
接下来看看View中的onMeasure方法。调用了setMeasuredDimension来存储测量宽和测量高。
自定义控件,从LinearLayout源码谈起_第5张图片

自定义控件,从LinearLayout源码谈起_第6张图片
被onMesure调用的,View中的getSuggestedMinimumWidth方法

注意:
1 可以看到view中的onMesure在两种MeasureSpec分别为At_MOST(对应LayoutParams中的wrapcontent)和EXACTLY(对应layoutparams中的matchparent或者fillparent)的情况下是一样的在getDefaultSize 方法中返回了result=specSize,也就是说直接继承View的自定义控件无论他的子view,wrapcontent,还是matchparent返回的测量尺寸大小都是一样的。显然这是不符合逻辑的,所以子view必须要做出自己的onMeasure操作。
2 **MesureSpec用于父元素对子元素进行控制,LayoutParams用于子元素告诉父元素自己想被怎么控制。**MesureSpec包含UNSPECIFIED,AT_MOST,EXACTLY。LayoutParams请看下面有详细介绍。
来看看LinearLayout的onMeasure操作,

3.2 LinearLayout的onMeasure流程

可以看到根据LinearLayout设置的方向(orientation)不同,分为了measureVertical和measureHorizontal,原理一样,这里分析measureVertical方法。这里代码注释截取自本篇博客,感谢原作者做的精彩分析

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;//子view的高度和
        int maxWidth = 0;//最大宽度
        int childState = 0;//child的状态
        int alternativeMaxWidth = 0;//子视图的最大宽度(不包含layout_weight>0的子view)
        int weightedMaxWidth = 0;//子视图的最大宽度(仅包含layout_weight>0的子view) 
        boolean allFillParent = true//子视图的宽度是否全是fillParent的,用于后续判断是否需要重新计算
        float totalWeight = 0//所有子view的weight之和  

        final int count = getVirtualChildCount();//实际的子view个数
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);//LinearLayout宽度模式 
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec); //LinearLayout高度模式 
        boolean matchWidth = false;
        //子view的宽度是否要由父确定。如父LinearLayout为layout_width=wrap_content,  子view为fill_parent则matchWidth =true  
        final int baselineChildIndex = mBaselineAlignedChildIndex;    //以LinearLayout中第几个子view的baseLine作为LinearLayout的基准线   
        final boolean useLargestChild = mUseLargestChild;//使用高度的child

        int largestChildHeight = Integer.MIN_VALUE;//设定高度最高的child的默认值

        //获得子view的高度,并记下最大的高度 
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);

            if (child == null) {
                mTotalLength += measureNullChild(i);//默认返回0

                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);//默认返回0
               continue;
            }

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

            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
//获取LinearLayout定义的LayoutParams
            totalWeight += lp.weight;//计算总共的权重weight

            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                // Optimization: don't bother measuring children who are going to use
                // leftover space. These views will get measured again down below if
                // there is any leftover space.
                //如果LinearLayout高度是已经确定的。并且这个子view的height=0,weight>0,  
                //则mTotalLength只需要加上margin即可,  
                //由于是weight>0;该view的具体宽度等会还要计算  
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            } else {
                int oldHeight = Integer.MIN_VALUE;

                if (lp.height == 0 && lp.weight > 0) {
                    // heightMode is either UNSPECIFIED or AT_MOST, and this
                    // child wanted to stretch to fill available space.
                    // Translate that to WRAP_CONTENT so that it does not end up
                    // with a height of 0
                    oldHeight = 0;
                    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).
                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 applicable, compute the additional offset to the child's baseline * we'll need later when asked {@link #getBaseline}. */
            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }

            // if we are trying to use a child index for our baseline, the above
            // book keeping only works if there are no children above it with
            // weight. fail fast to aid the developer.
            if (i < baselineChildIndex && lp.weight > 0) {
              //为什么i < baselineChildIndex && lp.weight > 0不行。  
                //假如行的话,如果LinearLayout与其他view视图对其的话,  
                //由于weight>0的作用,会影响其他所有的view位置  
                //应该是由于效率的原因才不允许这样。  
                throw new RuntimeException("A child of LinearLayout with index "
                        + "less than mBaselineAlignedChildIndex has weight > 0, which "
                        + "won't work. Either remove the weight, or don't set "
                        + "mBaselineAlignedChildIndex.");
            }

            boolean matchWidthLocally = false;
            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                // The width of the linear layout will scale, and at least one
                // child said it wanted to match our width. Set a flag
                // indicating that we need to remeasure at least that view when
                // we know our width.
                //如果LinearLayout宽度不是已确定的,如是wrap_content,而子view是FILL_PARENT,  
                //则做标记matchWidth=true; matchWidthLocally = true;  
                matchWidth = true;
                matchWidthLocally = true;
            }

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);//最大子view的宽度
            childState = combineMeasuredStates(childState, child.getMeasuredState());
//子view宽度是否全是FILL_PARENT  
            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                /* * Widths of weighted Views are bogus if we end up * remeasuring, so keep them separate. */
//如父width是wrap_content,子是fill_parent,则子的宽度需要在父确定后才能确定。这里并不是真实的宽度  
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

        if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
            mTotalLength += mDividerHeight;
        }

        if (useLargestChild &&
                (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
            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));
            }
        }

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

        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds
        int delta = heightSize - mTotalLength;
        if (delta != 0 && totalWeight > 0.0f) {
            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
                    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
                        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));
                    }

                    // Child may now not fit in vertical dimension.
                    childState = combineMeasuredStates(childState, child.getMeasuredState()
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                }

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


            // We have no limit, so make all weighted views as tall as the largest child.
            // Children will have already been measured once.
            if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
                for (int i = 0; i < count; i++) {
                    final View child = getVirtualChildAt(i);

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

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

                    float childExtra = lp.weight;
                    if (childExtra > 0) {
                        child.measure(
                                MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
                                        MeasureSpec.EXACTLY),
                                MeasureSpec.makeMeasureSpec(largestChildHeight,
                                        MeasureSpec.EXACTLY));
                    }
                }
            }
        }

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

        maxWidth += mPaddingLeft + mPaddingRight;

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

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);//最后一定要调用setMeasureDimension

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

这里对onMeasure中的一些片段进行分析:
如果LinearLayout高度是已经确定的。并且这个子view的height=0,weight>0,  则mTotalLength只需要加上margin即可,由于是weight>0;该view的具体高度等会还要计算  。
自定义控件,从LinearLayout源码谈起_第7张图片
如果MesureSpec的宽度模式是UNSPECIFIED或者AT_MOST,也就是说子元素想自定义宽,但是lp .width是0,但是子元素weight不为零,所以说,这时候还不能确定它的宽度,但是不能把它宽度置为0.这也就是我们遇见的,如果利用了weight属性,会导致linearLayout测量两次。
measureChildBeforeLayout则是结合父元素传递MeasureSpec,以及padding和Margin来完成了child的初次测量。margin和padding也是我们覆写viewGroup需要注意的地方。
自定义控件,从LinearLayout源码谈起_第8张图片
这里将resolveSizeAndState传递给View,然后View会返回一个标志位boolean值。
自定义控件,从LinearLayout源码谈起_第9张图片
onMesure方法中比较重要的一个步骤,将MesureSpec传递给child,也就实现了测量向下传递。我们的高度已经通过mTotalLength确定。通过child.measure也可以测量宽度。
自定义控件,从LinearLayout源码谈起_第10张图片
最后也是最重要的setMeasuredDimension方法。
自定义控件,从LinearLayout源码谈起_第11张图片

注意:
1 覆盖onMeasure方法时,必须调用 setMeasuredDimension(int, int)方法来保存评估结果的视图的宽度和高度.如果忘记将导致 measure(int, int)方法抛出IllegalStateException异常.
2 覆写onMesure需要让遍历所有子view,并让它们测量自身,才能实现整个measure测量的向下传递。
3 margin和padding也是我们覆写viewGroup需要注意的地方。

4 源代码解析之定义LayoutParams

想必你肯定在使用RelativeLayout或者LinarLayout的时候使用过各种LayoutParams,比如android:layout_height,android:layout_weight属性。LayoutParams是子元素用来用于告诉父元素它(子元素)想怎么摆放。基础的LayoutParams描述了view的宽高,对于宽和高我们可以指定为定义xml布局的时候进行的操作,wrap_content, match_parent,fill_parent 或者是一个具体的数值, 这也就是我们使用布局文件的时候进行的那些操作。
自定义控件,从LinearLayout源码谈起_第12张图片

1 ViewGroup的子类都会有自己的LayoutParams来定义自己的x,y以及它所特有的属性。
2 View 的绘制流程是从ViewRoot的PerformTraversal开始依次往下层的View传递的,这也就是我们需要通过LayoutParams来告诉父元素(ViewGroup)我们子元素(ViewGroup下面的view或者是ViewGroup)想怎么摆放的原因,不然你老爸怎么知道该在东边还是西边把你生出来?就是是说覆写ViewGroup必须有自己的LayoutParams。
3 可以这样去形容LayoutParams,在象棋的棋盘上,每个棋子都占据一个位置,也就是每个棋子都有一个位置的信息,如这个棋子在4行4列,这里的“4行4列”就是棋子的LayoutParams。

5 源码解析之layout过程

依然可以看到是分为两个方向进行layout。分析layoutVertical

void layoutVertical(int left, int top, int right, int bottom) {
//layout四个参数分别为左上右下 
 final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

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

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

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

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

layout相比measure过程就简单很多,主要是根据不同的gravity属性来确定子元素的child的位置,所以最后通过setChildFrame也就完成了整个layout过程

1 无论是onLayout还是onMesure,都会用到上面提到的LayoutParams。
2 onLayout最后通过child.layout方法来确定子view的位置。

6 源码解析之onDraw过程

对于继承ViewGroup,其实一般情况下是不需要重写onDraw方法的,让子元素自己去绘制自身就行。但是背景还有这里的divider就需要自行绘制了。

好了。写了整整一天的一篇文章,反复整理了几次,还是发现很多逻辑感觉不清晰,感觉源代码分析,轻重还是掌握不好。但是希望对看到的人有帮助。再接再厉,希望有问题的地方,能得到大家的指点迷津。最后,再来看看我这个简单的demo卡片布局CascadLayout

你可能感兴趣的:(源码,android,控件)