Android常用Layout源碼總結—LinearLayout

前言

通過學習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>
  • baselineAlignedChildIndex
    以某一個child的文字基線作爲基準對齊。
    例:
    在這裏插入圖片描述
    根佈局爲水平LinearLayout,紫塊、藍塊、綠塊是三個垂直LinearLayout,分別設置baselineAlignedChildIndex屬性值爲2、0、1,意味着紫塊以索引2即第三個child的文字基線作爲基準線,藍塊以第一個child爲基準,綠塊以第二個child爲基準,進行對齊。

  • measureWithLargestChild
    使所有設置了權重且爲設置精確尺寸的child的尺寸統一成最大的那個child的尺寸。
    例:

  • 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>

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調用測量。

發佈了27 篇原創文章 · 獲贊 2 · 訪問量 8505
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章