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

96
安东尼_Anthony 595a1b60 08f6 4beb 998f 2bf55e230555
0.1 2016.04.14 17:41* 字数 2025

本博客原地址:http://www.jianshu.com/p/f9b9f05222a8

(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

(哈哈,这里偷换概念,把系统的LinearLayout当做自定义控件,主要是一个学习目的。)。自定义ViewGroup的一个简单项目,请参考我的文章,卡片布局CascadLayout

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

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

LinearLayout定义了三个构造函数
LinearLayout的构造函数

LinearLayout的主要属性

这也是我们自定义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。

View类中的measure方法

接下来看看View中的onMeasure方法。调用了setMeasuredDimension来存储测量宽和测量高。
View中的onMeasure方法

被onMesure 调用的,View中的getDefaultSize方法

被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的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的具体高度等会还要计算 。


如果MesureSpec的宽度模式是UNSPECIFIED或者AT_MOST,也就是说子元素想自定义宽,但是lp .width是0,但是子元素weight不为零,所以说,这时候还不能确定它的宽度,但是不能把它宽度置为0.这也就是我们遇见的,如果利用了weight属性,会导致linearLayout测量两次。
measureChildBeforeLayout则是结合父元素传递MeasureSpec,以及padding和Margin来完成了child的初次测量。margin和padding也是我们覆写viewGroup需要注意的地方。

这里将resolveSizeAndState传递给View,然后View会返回一个标志位boolean值。

onMesure方法中比较重要的一个步骤,将MesureSpec传递给child,也就实现了测量向下传递。我们的高度已经通过mTotalLength确定。通过child.measure也可以测量宽度。

最后也是最重要的setMeasuredDimension方法。

注意:
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 中的LayoutParams 继承ViewGroup 的LayoutParams

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过程

建立在view的measure之上,并且利用measure测量的参数作为依据。

5.1View类的measure流程

通过下图,我们可以看到view的layout方法调用了onLayout(),onLayoutChange()。

View类中的layout方法

5.2LinearLayout的onMeasure流程

依然可以看到是分为两个方向进行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的位置。
3 每一个父控件都调用layout方法去放置或者布局自己的子控件,子控件通过在自己的onLayout方法中回调layout方法实现了循环遍历所有子孙控件的位置摆放。
4layout过程中还是要注意的是margin值和padding值。

6 源码解析之onDraw过程

在调用View类的draw流程的时候,一定要确保layout已经完成。不过这个不用我们去关心。只用去覆写onDraw就行了。


自定义控件请覆写onDraw方法

6.1View类的draw流程

view的绘制过程肯定也是需要遍历的,大概分为六个步骤,其中2和5一般情况下可以跳过,请看下面两张图:
1. Draw the background(绘制背景)
2. If necessary, save the canvas' layers to prepare for fading(保存画布的图层来准备色变)
3. Draw view's content(绘制内容)
4. Draw children(绘制children)
5. If necessary, draw the fading edges and restore layers(画出褪色的边缘和恢复层)
6. Draw decorations (scrollbars for instance)(绘制装饰 比如scollbar)




第3个步骤绘制当前内容的时候就会调用onDraw方法,可以看到view类中的onDraw方法是空实现。


view类中的onDraw方法

6.2LinearLayout的onDraw流程

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

2自定义控件继承自ViewGroup一般不需要绘制自身,也就是不需要覆写onDraw方法,但是需要管理绘制子View,也就是在dispatchDraw方法中进行子控件的控制。这个方法已经在ViewGroup类中进行了实现。

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

Android基础
Web note ad 1