Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 手把手教你實現 LinearLayout

手把手教你實現 LinearLayout

編輯:關於Android編程

前言

做了有一年的 android 應用開發了,一直停留在應用面,感覺好像也沒什麼提升了。正好最近不是特別忙,准備研究一下 android sdk 的源碼。手動實現一下 android 的原生控件等,當作一個系列來寫吧。不知道能不能堅持。就從比較簡單的 LinearLayout 開始吧。
LinearLayout 相信每個做 android 開發的肯定都不陌生。本篇也不准備把 LinearLayout 的每個屬性都來講解。就看他的 measure 以及 layout 部分,以及 weight 是如何應用到布局中去的。同時,我也不准直接粘 LinearLayout 的原生源碼來看,而是手動實現一個 LinearLayout,當然,大部分源碼是復制的 LinearLayout 的源碼。

准備工作

本篇適合對 View 的繪制有一定了解的人閱讀。不然我覺得可能有那麼一點吃力。
本篇源碼只實現 LinearLayout 的以下功能:

豎直方向上的測量以及布局 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~!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved