Android常用Layout源码总结—LinearLayout

Android常用Layout源码总结—LinearLayout通过学习Android官方Layout的源码,可以帮助自己更好的理解Android的UI框架系统,了解内部便捷的封装好的API调用,有助于进行布局优化和自定义view实现等工作。这里把学习结果通过写博客进行总结,便于记忆,不至于将来遗忘。 LinearLayout是Androi…

前言

通过学习Android官方Layout的源码,可以帮助自己更好的理解Android的UI框架系统,了解内部便捷的封装好的API调用,有助于进行布局优化和自定义view实现等工作。这里把学习结果通过写博客进行总结,便于记忆,不至于将来遗忘。

本篇博客中源码基于Android 8.1

LinearLayout特点

LinearLayout是Android开发中最常用的Layout之一,它支持水平或垂直线性布局,并且支持child设置权重weight,使child能够在主轴按一定比例填充LinearLayout。

源码探究

布局属性

首先查看LinearLayout的构造函数源码,在其中获取LinearLayout特有的布局属性:

public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);

    final TypedArray a = context.obtainStyledAttributes(
            attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);

    // 水平或垂直方向(0:HORIZONTAL,1:VERTICAL)
    int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
    if (index >= 0) {
        setOrientation(index);
    }

    // child的对齐方式
    index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1);
    if (index >= 0) {
        setGravity(index);
    }

    // 所有文本child以文字基线对齐
    boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true);
    if (!baselineAligned) {
        setBaselineAligned(baselineAligned);
    }

    // 总权重(若没有设置,则计算child的权重之和)
    mWeightSum = a.getFloat(R.styleable.LinearLayout_weightSum, -1.0f);

    // 指定以某个child的文字基线作为基准线对齐
    mBaselineAlignedChildIndex =
            a.getInt(com.android.internal.R.styleable.LinearLayout_baselineAlignedChildIndex, -1);

    // 所有设置权重且无精确尺寸的child,修改他们的尺寸和最大的child的尺寸一致
    mUseLargestChild = a.getBoolean(R.styleable.LinearLayout_measureWithLargestChild, false);

    // 显示分割线
    mShowDividers = a.getInt(R.styleable.LinearLayout_showDividers, SHOW_DIVIDER_NONE);
    // 分割线距两端的间距
    mDividerPadding = a.getDimensionPixelSize(R.styleable.LinearLayout_dividerPadding, 0);
    // 保存分割线的Drawable和宽高
    setDividerDrawable(a.getDrawable(R.styleable.LinearLayout_divider));

    final int version = context.getApplicationInfo().targetSdkVersion;
    mAllowInconsistentMeasurement = version <= Build.VERSION_CODES.M;

    a.recycle();
}

属性说明:

  • orientation child在LinearLayout中的水平或垂直排列方式。

  • gravity child在LinearLayout的对齐方式。

  • baselineAligned child以文字基线对齐。 例:

    baselineAligned图例

  • weightSum 权重总和,不设置的话将计算各child的weight之和。 例:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:weightSum="1"
    android:orientation="vertical"
    android:background="#dcdcdc">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="0.6"
        android:background="#3399cc"/>
</LinearLayout>

Android常用Layout源码总结—LinearLayout

  • baselineAlignedChildIndex 以某一个child的文字基线作为基准对齐。 例:

    在这里插入图片描述

    根布局为水平LinearLayout,紫块、蓝块、绿块是三个垂直LinearLayout,分别设置baselineAlignedChildIndex属性值为2、0、1,意味着紫块以索引2即第三个child的文字基线作为基准线,蓝块以第一个child为基准,绿块以第二个child为基准,进行对齐。

  • measureWithLargestChild 使所有设置了权重且为设置精确尺寸的child的尺寸统一成最大的那个child的尺寸。 例: Android常用Layout源码总结—LinearLayout Android常用Layout源码总结—LinearLayout

  • showDividers 显示分割线,支持设置beginning(分割线位于第一个child前面)、end(分割线位于最后一个child后面)、middle(分割线位于每个child之间)、none(默认,不显示分割线)。

  • dividerPadding 若显示分割线,设置分割线距两端的间距。

  • divider 设置分割线的图案。 例:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@drawable/shape_line"
    android:dividerPadding="30dp"
    android:showDividers="middle"
    android:orientation="vertical" >

    <View
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="#e8eaf6"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="#e0f2f1"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="#e8f5e9"/>
</LinearLayout>

Android常用Layout源码总结—LinearLayout

LayoutParams

LinearLayout中定义了静态内部类LayoutParams继承自MarginLayoutParams,定义了两个成员weight、gravity:

public float weight;
public int gravity = -1;

因此支持child设置权重和对齐方式。

onMeasure测量

LinearLayout在测量过程中会根据child的LayoutParams进行多次测量,测量流程较长,这里把测量分为预测量和补充测量。

开始测量

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 根据排列方向,执行不同的测量方法。
    if (mOrientation == VERTICAL) {
        // 垂直排列
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        // 水平排列
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

measureVertical和measureHorizontal方法内的逻辑类似 这里以垂直排列为例。

准备初始参数阶段

进入measureVertical方法,首先初始一些变量,用作辅助测量计算:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // 内容总高度(所有child的测量高度总和+divider高度+边距)
    mTotalLength = 0;
    // 最大宽度(最大child宽度+边距)
    int maxWidth = 0;
    // child测量状态(可设置MEASURED_STATE_TOO_SMALL标识位,用于向父布局请求加大宽高)
    int childState = 0;
    // 备选最大宽度(记录非权重的child最大宽度)
    int alternativeMaxWidth = 0;
    // 权重最大宽度(记录含权重的child最大宽度)
    int weightedMaxWidth = 0;
    // 标记是否所有child的LayoutParams.width为MATCH_PARENT
    boolean allFillParent = true;
    // 计算child的权重之和
    float totalWeight = 0;

    // child数量(该方法内直接调用getChildCount,子类可重写该方法进行)
    final int count = getVirtualChildCount();

    // 取出父布局传入的测量规格模式。
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    // 标记是否在LinearLayout的宽度确定后,对LayoutParams.width为MATCH_PARENT的child进行再次测量。
    boolean matchWidth = false;
    // 标记是否有对某个child暂不测量。
    boolean skippedMeasure = false;

    // baselineAlignedChildIndex属性值,默认为-1。
    final int baselineChildIndex = mBaselineAlignedChildIndex;
    // measureWithLargestChild属性值,默认为false。
    final boolean useLargestChild = mUseLargestChild;

    // 最大child的高度,当useLargestChild为true时有用。
    int largestChildHeight = Integer.MIN_VALUE;
    // 记录设置了LayoutParams.height为0像素且权重大于0的child占用的总高度
    int consumedExcessSpace = 0;

    // 记录在第一轮遍历child时,有效child个数。
    int nonSkippedChildCount = 0;
    
    // 省略其余部分
    // ···
}

预测量阶段

在准备完参数变量后,接下来开始遍历子view:

void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
    // 省略上面部分
    // ···
    
    // See how tall everyone is. Also remember max width.
    // 遍历测量子view,同时比较出最宽的子view的宽度。
    for (int i = 0; i < count; ++i) {
        // 根据索引返回子view(该方法内直接调用getChildAt,子类可重新该方法扩展,因此有可能返回null)。
        final View child = getVirtualChildAt(i);
        if (child == null) {
            // 跳过空的child,measureNullChild固定返回0。
            mTotalLength += measureNullChild(i);
            continue;
        }

        if (child.getVisibility() == View.GONE) {
           // 跳过GONE的child,getChildrenSkipCount固定返回0,子类可重写使之跳过后续的child。
           i += getChildrenSkipCount(child, i);
           continue;
        }

        // 完成非null和非GONE检查后,计数加一,该计数用于后续divider的判断
        nonSkippedChildCount++;
        // 首先判断是否计算divider高度
        // hasDividerBeforeChildAt根据当前索引和showDividers属性值
        // 判断当前位置是否有divider(可能存在beginning和middle两个位置)。
        if (hasDividerBeforeChildAt(i)) {
            // 若有divider,需要增加divider的高度。
            mTotalLength += mDividerHeight;
        }

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

        // 累加child总权重
        totalWeight += lp.weight;

        // 标记该child是否使用剩余空间(当child设置高度为0像素且设置大于0的权重,则LinearLayout先分配空间给其他child,
        // 之后再用剩余空间进行权重分配)。
        final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
            // 若高度模式为EXACTLY,表示LinearLayout自身的高度大小是可以确定的(LinearLayout自身的LayoutParams.height
            // 设置了精确的像素尺寸。或者是MATCH_PARENT,但LinearLayout的父布局的高度也是确定的)。
            // 且同时该child被标记使用剩余空间,则先不测量和获取child的测量高度,仅计算margin。
            
            // Optimization: don't bother measuring children who are only
            // laid out using excess space. These views will get measured
            // later if we have space to distribute.
          
            // 这里可以优化的原因是因为LinearLayout的LayoutParams.height是可以明确的,也不会是WRAP_CONTENT,
            // 无需知道child自身内容高度来计算LinearLayout自身高度。而child的LayoutParams.height为0像素,
            // 完全依赖LinearLayout剩余空间分配权重,若剩余空间为0,则child也不会显示。
          
            final int totalLength = mTotalLength;
            // 这里取大值是为了避免margin为负数。
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            // 标记skippedMeasure,用于后续判断补充测量。
            skippedMeasure = true;
        } else {
            // 常规测量流程
            if (useExcessSpace) {
                // The heightMode is either UNSPECIFIED or AT_MOST, and
                // this child is only laid out using excess space. Measure
                // using WRAP_CONTENT so that we can find out the view's
                // optimal height. We'll restore the original height of 0
                // after measurement.
                // 进入这个条件的话,高度模式只能是UNSPECIFIED或AT_MOST,且child被标记使用剩余空间。
                // 在child测量前把LayoutParams.height临时改为WRAP_CONTENT,使child能够测量自身内容需要的高度,测量完成后再改回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).
            // 已占用的高度,如果截至到当前,都没有child设置大于0的权重,赋值为总内容高度,否则赋值为0。
            final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
            // 第一次调用child测量。
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);

            // 获取child测量后的高度。
            final int childHeight = child.getMeasuredHeight();
            if (useExcessSpace) {
                // Restore the original height and record how much space
                // we've allocated to excess-only children so that we can
                // match the behavior of EXACTLY measurement.
                // 恢复LayoutParams.height为0。
                lp.height = 0;
                // 记录消耗的高度,用于后续计算剩余空间。(为什么这里单独记录消耗高度?是因为下面累加内容总高度时,
                // 也会加进这个child高度,而这个child实际是需要等待后续有剩余空间后再分配高度,因此后续计算时会加上这个值)
                consumedExcessSpace += childHeight;
            }

            final int totalLength = mTotalLength;
            // 累加总内容高度。
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                   lp.bottomMargin + getNextLocationOffset(child));

            // 若设置了measureWithLargestChild属性,记录最大的child高度。(默认不执行,可忽略)
            if (useLargestChild) {
                largestChildHeight = Math.max(childHeight, largestChildHeight);
            }
        }

        // 省略baselineChildIndex属性逻辑部分,默认不执行,不影响主测量流程。
        // ···
      
        // 调用child测量结束,下面是处理宽度相关的逻辑。
        // 用于标记该child的测量宽度是否有效。
        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.
            // 此时child为MATCH_PARENT需要依赖父布局的宽度,但父布局自身宽度也不确定。所以标记matchWidth、matchWidthLocally为true,
            // 后面将再次调用child测量。
            matchWidth = true;
            matchWidthLocally = true;
        }

        // 计算child的margin
        final int margin = lp.leftMargin + lp.rightMargin;
        // child的测量宽度加上margin
        final int measuredWidth = child.getMeasuredWidth() + margin;
        // 记录最大的child宽度
        maxWidth = Math.max(maxWidth, measuredWidth);
        // 合并child的测量状态
        childState = combineMeasuredStates(childState, child.getMeasuredState());

        // 记录是否所有child的宽度都为MATCH_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. */
          	// 若上面标记该child的测量宽度无效,则仅用margin参与比较。
            weightedMaxWidth = Math.max(weightedMaxWidth,
                    matchWidthLocally ? margin : measuredWidth);
        } else {
            // 若上面标记该child的测量宽度无效,则仅用margin参与比较。
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                    matchWidthLocally ? margin : measuredWidth);
        }

        // getChildrenSkipCount方法内直接返回0,子类可重写使之跳过后续的child。
        i += getChildrenSkipCount(child, i);
    }
  
    // 省略剩余部分
    // ···
}

这部分逻辑主要是遍历child测量,同时记录总高度和最大child宽度。对于设置了权重的child,这次遍历测量并未真正根据权重分配空间,并且对于满足特点条件的child先暂时不调用child的测量方法。

补充测量阶段

接下来会根据第一轮的测量情况,对子view进行补充测量:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // 省略第一轮遍历测量部分
    // ···
    // 省略判断divider部分(通过hasDividerBeforeChildAt方法判断是否有end位置的divider,若有,内容总高度需要加上dividerHeight)
    // ···
    // 省略measureWithLargestChild属性部分(重新计算内容总高度,每个child的测量高度都用前面部分记录的最大child高度替代)
    // ···

    // 内容总高度再加上LinearLayout自身的上下内边距。
    // 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. If we skipped
    // measurement on any children, we need to measure them now.
    // 计算剩余空间(mAllowInconsistentMeasurement在高于M的版本上为false)
    // 注意:remainingExcess有可能为负数,因为经过resolveSizeAndState的调整,heightSize可能远小于mTotalLength。
    int remainingExcess = heightSize - mTotalLength
            + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
    if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
        // 需要根据权重分配空间
        // 计算剩余总权重(若设置了weightSum属性,以weightSum为准,否则计算出来的child总权重为准)
        float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

        // 内容总高度清零
        mTotalLength = 0;

        // 第二次遍历子view
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null || child.getVisibility() == View.GONE) {
                continue;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final float childWeight = lp.weight;
            // 对权重大于0的child,计算权重分配空间。
            if (childWeight > 0) {
                // 根据权重比例从剩余空间求出可分配高度(有可能为负值,因此有可能出现权重大的child,实际高度反而小的情况)。
                final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
                // 计算剩余高度和剩余总权重
                remainingExcess -= share;
                remainingWeightSum -= childWeight;

                // 计算child的高度,后面会以此值参与生成child的高度测量规格。
                final int childHeight;
                if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
                    // 使用最大的child高度,默认为false,可忽略。
                    childHeight = largestChildHeight;
                } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
                        || heightMode == MeasureSpec.EXACTLY)) {
                    // This child needs to be laid out from scratch using
                    // only its share of excess space.
                    // 若child的高度为0像素,且LinearLayout自身可确定高度,child高度为按权重分配的高度。
                    childHeight = share;
                } else {
                    // This child had some intrinsic height to which we
                    // need to add its share of excess space.
                    // child高度为child测量后高度加上按权重分配高度(因此child并不是完全按照权重比例高)。
                    childHeight = child.getMeasuredHeight() + share;
                }

                // 使用childHeight直接生成高度测量规格,此时指定明确高度,并且保证高度不为负数。
                final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        Math.max(0, childHeight), MeasureSpec.EXACTLY);
                // 通过getChildMeasureSpec方法按照系统默认规则生成宽度测量规格。
                final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                        lp.width);
                // 调用child测量。此时高度是明确清楚的了。
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

                // Child may now not fit in vertical dimension.
                // 组合child测量状态。
                childState = combineMeasuredStates(childState, child.getMeasuredState()
                        & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
            }

            // 记录最大child宽度
            final int margin =  lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);

            // 标记child的测量宽度是否有用
            boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                    lp.width == LayoutParams.MATCH_PARENT;

            // 记录最大备选宽度
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                    matchWidthLocally ? margin : measuredWidth);

            // 记录是否所有child的是MATCH_PARENT
            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);

        // 省略measureWithLargestChild属性部分(统一以最大child高度对子view进行测量)
        // ···
    }
    
    // 省略测量尾声部分
    // ···
}

这部分主要是针对权重进行分配空间补充测量,若预测量阶段有未测量的child,或child总权重大于0且有剩余空间,则执行。

从源码中可以看到,用于权重分配的高度,是由LinearLayout自身高度减去在预测量阶段确定的总内容高度求出剩余空间高度。各个child的高度不是严格和权重比例一致,而是child自身高度加上权重高度。当child的总高度超过LinearLayout高度时,权重高度会出现负数,因此会出现权重越大的child,高度反而越小

测量尾声阶段

前面部分依次对child进行了测量,并且在过程中记录了最大child宽度和生成了高度规格,接下来便要设置LinearLayout自身的尺寸:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // 省略前面测量部分
    // ···

    if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
        // 若不是所有child都为MATCH_PARENT且LinearLayout宽度未明确
        maxWidth = alternativeMaxWidth;
    }

    // 最大宽度加上内边距
    maxWidth += mPaddingLeft + mPaddingRight;

    // Check against our minimum width
    // 确保宽度不小于最小宽度
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // 设置LinearLayout自身尺寸
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);

    if (matchWidth) {
        // 若在前面测量阶段中,标记有child的宽度需要依赖LinearLayout的宽度,则在最后需要再进行一次测量。
        forceUniformWidth(count, heightMeasureSpec);
    }
}

这部分就是给LinearLayout自身设置了尺寸,但是在最后,还要处理下先前阶段中LayoutParams.width为MATCH_PARENT的child,因为其依赖LinearLayout的宽度而没能准确测算宽度,对他们再次测量:

private void forceUniformWidth(int count, int heightMeasureSpec) {
    // Pretend that the linear layout has an exact size.
    // 获取LinearLayout自身的宽度,生成宽度规格。
    int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
            MeasureSpec.EXACTLY);
    // 遍历子view
    for (int i = 0; i< count; ++i) {
       final View child = getVirtualChildAt(i);
       if (child != null && child.getVisibility() != GONE) {
           LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams());

           // 测量MATCH_PARENT的child
           if (lp.width == LayoutParams.MATCH_PARENT) {
               // Temporarily force children to reuse their old measured height
               // FIXME: this may not be right for something like wrapping text?
               int oldHeight = lp.height;
               // 临时修改child的LayoutParams.height为child的测量高度,因为当前child的高度已经测量完成。
               lp.height = child.getMeasuredHeight();

               // Remeasue with new dimensions
               // 调用child测量
               measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
               lp.height = oldHeight;
           }
       }
    }
}

onLayout布局

布局也是根据排列方向执行不同的方法:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

layoutVertical和layoutHorizontal布局逻辑相似,这里以垂直排列为例:

void layoutVertical(int left, int top, int right, int bottom) {
    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
    // 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
           // 靠底部,上边界需向下偏移,偏移距离即为LinearLayout高度减去内容高度的空白区域高度。
           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;
    }

    // 遍历子view,一次调用child.layout
    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();

            // 获取child设置的LayoutParams的对齐方式
            int gravity = lp.gravity;
            if (gravity < 0) {
                // 若没有设置,则以LinearLayout的水平方向对齐方式为准。
                gravity = minorGravity;
            }
            // 获取内容布局方向(RTL或LTR)
            final int layoutDirection = getLayoutDirection();
            // 转换相对对齐方式(将START、END转换成LEFT、RIGHT)
            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;
            }

            // 判断该索引位置是否有divider,若有,上边界需要偏移DividerHeight。
            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin;
            // setChildFrame方法内直接调用child的layout进行布局。
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
            // getChildrenSkipCount方法返回0
            i += getChildrenSkipCount(child, i);
        }
    }
}

LinearLayout的布局方法较简单,按照对齐方式,对边界进行偏移。并线性对child进行排布,不断向下偏移上边界。

onDraw绘制

@Override
protected void onDraw(Canvas canvas) {
    // 若没有设置分割线,则直接返回
    if (mDivider == null) {
        return;
    }

    if (mOrientation == VERTICAL) {
        drawDividersVertical(canvas);
    } else {
        drawDividersHorizontal(canvas);
    }
}

看源码可知,LinearLayout的绘制只针对分割线。绘制同样分为不同方向,以垂直方向为例:

void drawDividersVertical(Canvas canvas) {
    final int count = getVirtualChildCount();
    // 遍历子view
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child != null && child.getVisibility() != GONE) {
            // 判断该索引位置是否有divider
            if (hasDividerBeforeChildAt(i)) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int top = child.getTop() - lp.topMargin - mDividerHeight;
                // 给divider设置Bounds,绘制在canvas上。
                drawHorizontalDivider(canvas, top);
            }
        }
    }

    // 遍历完子view后,再判断是有有end位置的divider。
    if (hasDividerBeforeChildAt(count)) {
        final View child = getLastNonGoneChild();
        int bottom = 0;
        if (child == null) {
            bottom = getHeight() - getPaddingBottom() - mDividerHeight;
        } else {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            bottom = child.getBottom() + lp.bottomMargin;
        }
        drawHorizontalDivider(canvas, bottom);
    }
}

总结

LinearLayout核心、复杂的逻辑主要在测量流程中,因为权重的出现,可能导致对child进行多次测量。(这里对垂直方向进行总结,水平方向逻辑相似)

child不设置权重的情况下,LinearLayout只需按照ViewGroup常规的测量流程,依次调用child测量,再计算内容高度和宽度,最后结合测量规格设置自身尺寸即可。

在含有权重child后,情况变得复杂。首先进行一次预测量,期间求出内容高度。之后进行补充测量,用LinearLayout的测量规格高度减去内容总高度后求出剩余高度,剩余高度再按照权重比例分配高度,让child的高度再加上这部分高度。最后设置LinearLayout自身尺寸。

注意,不管LinearLayout有没有权重child。若在测量期间,有child的LayoutParams.width为MATCH_PARENT,且LinearLayout的宽度测量规格不为EXACTLY。意味着child需要依赖父布局的宽度,但父布局此时宽度尚不明确。因此在LinearLayout设置完自身尺寸后,还会对这些child调用测量。

今天的文章Android常用Layout源码总结—LinearLayout分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/14575.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注