手把手教你实现 LinearLayout

前言

做了有一年的 android 应用开发了,一直停留在应用面,感觉好像也没什么提升了。正好最近不是特别忙,准备研究一下 android sdk 的源码。手动实现一下 android 的原生控件等,当作一个系列来写吧。不知道能不能坚持。就从比较简单的 LinearLayout 开始吧。
LinearLayout 相信每个做 android 开发的肯定都不陌生。本篇也不准备把 LinearLayout 的每个属性都来讲解。就看他的 measure 以及 layout 部分,以及 weight 是如何应用到布局中去的。同时,我也不准直接粘 LinearLayout 的原生源码来看,而是手动实现一个 LinearLayout,当然,大部分源码是复制的 LinearLayout 的源码。

准备工作

本篇适合对 View 的绘制有一定了解的人阅读。不然我觉得可能有那么一点吃力。
本篇源码只实现 LinearLayout 的以下功能:

  1. 竖直方向上的测量以及布局
  2. weight 的实现

开始

ViewGroup 定义

首先,我们自定义一个 ViewGroup 命名为MyLinearLayout,继承自 ViewGroup。同时,我们知道,一个 ViewGroup,必然对应着一个 LayoutParams,我们在 MyLinearLayout 定义一个静态内部类,MyLinearLayoutParams,继承自 MarginLayoutParams。为什么继承自 MarginLayoutParams,因为我们得使用 margin 属性啊。这一部分的源码如下。



 public class MyLinearLayout extends ViewGroup {
    private static final String TAG = "MyLinearLayout";
    //子 View 的总高度,注意,这个高度不等于布局高度
    private int mTotalLength = 0;
    private float mWeightSum = 0;
    public MyLinearLayout(Context context) {
        super(context);
    }

    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MyLinearLayout);
        mWeightSum = a.getFloat(R.styleable.MyLinearLayout_weightSum, 0);
        a.recycle();
    }

    public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MyLinearLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MyLinearLayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLinearLayoutParams(getContext(), attrs);
    }
    public static class MyLinearLayoutParams extends ViewGroup.MarginLayoutParams {

        public float weight;

        public MyLinearLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.MyLinearLayout);
            weight = a.getFloat(R.styleable.MyLinearLayout_weight, 0);
            a.recycle();
        }


        public MyLinearLayoutParams(int width, int height) {
            super(width, height);
        }

        public MyLinearLayoutParams(LayoutParams source) {
            super(source);

        }
    }

上面代码关于自定义属性的部分我就不说了,可自行百度。注意MyLinearLayoutParams 里面的变量 weight。关于 generateLayoutParams ,如果有不懂得人,那么我简单说下,一个 ViewGroup 中的 view,肯定需要一个 LayoutParams,你可以将 generateLayoutParams 理解为为每个子 View 生成一个 LayoutParams。

onMeasure

onMeasure 部分我们先从最简单的开始,先不对 weight 进行实现,只实现 vertical 的测量以及布局。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //由于可能进行多次 measure,所以每次测量前先把 mTotalLength 清空。
    mTotalLength = 0;
    int childCount = getChildCount();
    int maxWidth = 0;//最大的宽度,将作为布局的宽度
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        MyLinearLayoutParams lp = (MyLinearLayoutParams) child.getLayoutParams();
        if (child.getVisibility() == GONE) {
            //注意这里的+0并非没有意义,事实上源码中这里是调用了一个方法,只不过其结果返回0而已。
            //好像确实没什么卵用
            i += 0;
            continue;
        }
        //对子 View 进行测量,关于这个方法我不想展开,不然篇幅过长。简单说下内部实现。
        //其内部会根据 child 的 layoutParams 以及父布局的 measureSpec 生成一个 measureSpec,传递给子 View 的 measure 方法。
        //具体如何测量就交给子 View自己了。如果实在想懂,可以自己去看源码,也不是特别难。
        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, mTotalLength);
        int totalLength = mTotalLength;
        mTotalLength = Math.max(totalLength, mTotalLength +=
            (child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin));
        //选出宽度最大一个的宽度作为布局的宽度
        maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
    }
    //将宽度加上 padding,得到最终宽度
    maxWidth += getPaddingLeft() + getPaddingRight();
    mTotalLength += (getPaddingTop() + getPaddingBottom());
    //测量高度,布局高度其实从这里就已经确定了,不管你下面 weight 的测量了。
    //因为,假设布局的高度为精确值(Exactly),那么其高度就是精确值的高度。
    //如果为 wrap_content,则高度为子view 的测量高度之和,因此,由于高度没有剩余空间的,所以子 view 的 weight 失效
    //当然。总高度不会超过父布局的最大高度
    int heightSize = mTotalLength;
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    //对高度进行纠正,如果布局高度为 wrapContent,则最终高度为 mTotalLength 的大小
    //如果为布局高度为精确值(Exactly),则最终高度为精确值
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
    //测量宽度
    setMeasuredDimension(maxWidth, heightSize);
}

重申一下,mTotalLenght 不是最终布局的高度,这个高度最后还需要经过纠正,具体纠正细节可以到 resolveSizeAndState(heightSize, heightMeasureSpec, 0) 这个方
法中看下。

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

实现比较简单,具体如何实现在前一段的注释已经说了,只不过返回值是大小以及一个标志量的组合而已。
到这一步测量就完成了,接下来布局就不多说了,直接贴代码。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childTop = 0;
    int childLeft;
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        MyLinearLayoutParams lp = (MyLinearLayoutParams) child.getLayoutParams();
        if (child.getVisibility() == GONE) {
            childTop += 0;
            continue;
        }
        childLeft = lp.leftMargin;
        childTop += lp.topMargin;
        int childRight = child.getMeasuredWidth() + childLeft;
        int childBottom = childTop + 0 + child.getMeasuredHeight();
        child.layout(childLeft, childTop, childRight, childBottom);
        childTop += lp.bottomMargin + child.getMeasuredHeight() + 0;
    }
}

到这一步运行 App 就可以看到一个垂直布局的效果了。相信到这一步也没啥意思,估计很多人都知道怎么实现,那么接下来我们就来看看 weight 是如何实现的。

weight 的引入

首先,我们必须知道一件事,LinearLayout 的 onMeasure 方法里其实分为两个阶段,第一阶段其实是分配 子 View 的真实高度,即不考虑 weight 所占用的高度。第二阶段会对 weight 进行换算,对有 weight 属性的 view 进行测量。所以可以得出一个结论,如果一个 view,其 layout_height!=0,且其 weight 有值,那么这个子 view 是会进行两次测量的。那么直接贴源码。其实有一半跟上面那部分的 onMeasure 方法是一样的,只不过引入了 weight 的计算。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mTotalLength = 0;//子 view 加起来的总高度
    int childCount = getChildCount();
    int maxWidth = 0;//最大的宽度,将作为布局的宽度
    float totalWeight = 0;
    boolean skippedMeasure = false;//是否有跳过 measure 的 child view
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        MyLinearLayoutParams lp = (MyLinearLayoutParams) child.getLayoutParams();
        if (child.getVisibility() == GONE) {
            i += 0;
            continue;
        }
        // 累加 weight 的值
        totalWeight += lp.weight;
        //这个判断条件我们应该挺眼熟,通常我们设置
        if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            skippedMeasure = true;
        } else {
            if (lp.height == 0 && lp.weight > 0) {
                //父布局是 wrap_content,为了避免子 view 测量出来的高度是 0(因为 lp.height=0),使其高度为 wrap_content
                lp.height = LayoutParams.WRAP_CONTENT;
            }
            //如果之前或者当前 view 有使用 weight,则我们允许他的大小为父布局的全部空间
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0);
            int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, mTotalLength +=
                (child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin));
        }
        //选出宽度最大一个的宽度作为布局的宽度
        maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
    }
    mTotalLength += (getPaddingTop() + getPaddingBottom());
    //测量高度,布局高度其实从这里就已经确定了,不管你下面 weight 的测量了。
    //因为,假设布局的高度为精确值,那么其高度就是精确值的高度。
    //如果为 wrap_content,则高度为子view 的测量高度之和,因此,由于高度没有剩余空间的,所以子 view 的 weight 失效
    //当然。总高度不会超过父布局的最大高度
    int heightSize = mTotalLength;
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
    //================这里开始 weight 测量的逻辑===================================
    //计算父布局的剩余空间
    int delta = heightSize - mTotalLength;
    //布局有剩余空间或者前面有跳过测量的 view,则进行 weight 的计算
    if (skippedMeasure || delta > 0 && totalWeight > 0) {
        float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
        // 这里把 mTotalLenght 置0,因为后面有一次重新遍历,那时再进行累加
        mTotalLength = 0;
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }
            MyLinearLayoutParams lp = (MyLinearLayoutParams) child.getLayoutParams();
            //取出子 view 的 weight
            float childExtra = lp.weight;
            //有 weight 属性,开始计算
            if (childExtra > 0) {
                //这里就验证了我们以前的结论,weight 所占的大小为父布局剩余空间*weight/weightSum
                int share = (int) (childExtra * delta / weightSum);
                //注意 weightSum 不是不变的,会随着每次循环减小
                weightSum -= childExtra;
                delta -= share;
                final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                    getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width);
                //如果以下判断能执行,证明此 view 在前面已经测量过一次。
                // 因为前面进行测量的条件是:heightMode != MeasureSpec.EXACTLY 或者 lp.height != 0 或者 childExtra = 0
                if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
                    //这句可以看出 layout_height 与 weight 可以同时起效
                    int childHeight = child.getMeasuredHeight() + share;
                    if (childHeight < 0) {
                        childHeight = 0;
                    }
                    //可以看出,如果 child 同时设置了 height 以及 weight,是需要两次测量的
                    // 这里不调用 measureChildWithMargins 的原因是大小已经确定了,没必要再判断了。
                    child.measure(childWidthMeasureSpec,
                        MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
                } else {
                    // 如果执行到这里,表示 该 view 之前没有进行过测量
                    child.measure(childWidthMeasureSpec,
                        MeasureSpec.makeMeasureSpec(share > 0 ? share : 0, MeasureSpec.EXACTLY));
                }
            }
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                lp.topMargin + lp.bottomMargin + 0);
        }

    }
    //测量宽度
    maxWidth += getPaddingLeft() + getPaddingRight();
    setMeasuredDimension(maxWidth, heightSize);
}

代码是多了一点,不过仔细看个几遍,大概也能明白,我这里大概梳理下。
首先第一次遍历子 View 进行测量的时候,并不是所有子 View 都会进行测量,如果满足父布局的
高度为精确值,子 View 高度为0且weight有值的时候,则跳过这个 view 的测量。注意,当父布局的高度为 wrap_content 的时候,如果子 view 高度为0,但 weight 有值,是需要对这个子 View 的高度进行处理的,将其设为 wrap_content,否则,这个子 view 的高度将为0。
这里先放上第一次测量的结论:
如果父布局的高度为精确值,则最终高度为精确值。如果父布局高度为 wrap_content,则其最终高度为所有子 View 的高度之和。
接着开始第二次测量,首先计算得到剩余的空间,这里如果父布局为 wrap_content 的话,则这里是没有剩余空间的,这也就解释了为什么当父布局为 wrap_content 的时候,weight 会失效了,因为压根不满足进入 weight计算的条件,即 skippedMeasure为true || delta > 0 && totalWeight > 0。进入 weight 的计算后,里面的逻辑相对就比较简单了,根据这条 weight * 剩余空间 / weightSum 公式去换算大小。这里要注意,如果一个子 view 的 (layout_height!=0||heightMode!=MeasureSpec.EXACTLY)&&weight!=0,则这个 view 在一个 measure 过程中是会经过两次测量的。

到这里,measure 过程就结束了,layout 过程代码不变,就不贴了。

总结

这篇博客主要是对 LinearLayout 的主要代码做个整理。事实上其 onMeasure 方法还有更多代码,对 baseLine的处理、divider 的处理等,同时方向为 vertical 时,宽度也没有我上面代码那样简单的计算而已,but,其实那些都不算难,把主要的过程疏通了之后,其他都不是难事啊。
最后,还是那句话,关于源码的博客,肯定不可能让你看到懂,只不过让你知道一个大概的方向,让你自己看源码时更加轻松而已。
The end~!

你可能感兴趣的:(手把手实现原生控件)