1. 程式人生 > >每日一問:LayoutParams 你知道多少?

每日一問:LayoutParams 你知道多少?

前面的文章中著重講解了 View 的測量流程。其中我提到了一句非常重要的話:View 的測量匡高是由父控制元件的 MeasureSpec 和 View 自身的 `LayoutParams 共同決定的。我們在前面的 每日一問:談談對 MeasureSpec 的理解 把 MeasureSpec 的重點進行了講解,其實另外一個 LayoutParams 同樣是非常非常重要。

從概念講起

LayoutParams,顧名思義,就是佈局引數。而且大多數人對此都是司空見慣,我們 XML 檔案裡面的每一個 View 都會接觸到 layout_xxx 這樣的屬性,這實際上就是對佈局引數的描述。大概大家也就清楚了,layout_

這樣開頭的東西都不屬於 View,而是控制具體顯示在哪裡。

LayoutParams 都有哪些初始化方法

通常來說,我們都會把我們的控制元件放在 XML 檔案中,即使我們有時候需要對螢幕做比較「取巧」的適配,會直接通過 View.getLayoutParams() 這樣的方法獲取 LayoutParams 的例項,但我們接觸的少並不代表它的初始化方法不重要。

實際上,用程式碼寫出來的 View 載入效率要比在 XML 中載入快上大約 1 倍。只是在如今手機配置都比較高的情況下,我們常常忽略了這種方式。

我們來看看 ViewGroup.LayoutParams 到底有哪些構造方法。

public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}

public LayoutParams(int width, int height) {
    this.width = width;
    this.height = height;
}

public LayoutParams(LayoutParams source) {
    this.width = source.width;
    this.height = source.height;
}

LayoutParams() {  }

MarginLayoutParams

除去最後一個放給 MarginLayoutParams 做處理的方法外,我們在 ViewGroup 中還有 3 個構造方法。他們分別負責給 XML 處理、直接讓使用者指定寬高、還有類似集合的 addAll() 這樣的方式的賦值方法。

實際上,ViewGroup 的子類的 LayoutParams 類擁有更多的構造方法,感興趣的自己翻閱原始碼檢視。在這裡我想更加強調一下我上面提到的 MarginLayoutParams

MarginLayoutParams 繼承於 ViewGroup.LayoutParams

public static class MarginLayoutParams extends ViewGroup.LayoutParams {
    @ViewDebug.ExportedProperty(category = "layout")
    public int leftMargin;

    @ViewDebug.ExportedProperty(category = "layout")
    public int topMargin;

    @ViewDebug.ExportedProperty(category = "layout")
    public int rightMargin;

    @ViewDebug.ExportedProperty(category = "layout")
    public int bottomMargin;

    @ViewDebug.ExportedProperty(category = "layout")
    private int startMargin = DEFAULT_MARGIN_RELATIVE;

    @ViewDebug.ExportedProperty(category = "layout")
    private int endMargin = DEFAULT_MARGIN_RELATIVE;

    public MarginLayoutParams(Context c, AttributeSet attrs) {
        super();
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
        setBaseAttributes(a,
                R.styleable.ViewGroup_MarginLayout_layout_width,
                R.styleable.ViewGroup_MarginLayout_layout_height);

        int margin = a.getDimensionPixelSize(
                com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
        if (margin >= 0) {
            leftMargin = margin;
            topMargin = margin;
            rightMargin= margin;
            bottomMargin = margin;
        } else {
            int horizontalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
            // ... something
        }
        // ... something
    }
}

一看程式碼,自然就清楚了,為什麼我們以前會發現在 XML 佈局裡, layout_margin 屬性的值會覆蓋 layout_marginLeftlayout_marginRight 等屬性的值。

實際上,事實上,絕大部分容器控制元件都是直接繼承 ViewGroup.MarginLayoutParams 而非 ViewGroup.LayoutParams。所以我們再自定義 LayoutParams 的時候記得繼承 ViewGroup.MarginLayoutParams

在程式碼裡面使用 LayoutParams

前面介紹了 LayoutParams 的幾種構造方法,我們下面以 LinearLayout.LayoutParams 來看看幾種簡單的使用方式。

val textView1 = TextView(this)
textView1.text = "不指定 LayoutParams"
layout.addView(textView1)

val textView2 = TextView(this)
textView2.text = "手動指定 LayoutParams"
textView2.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
layout.addView(textView2)

val textView3 = TextView(this)
textView3.text = "手動傳遞 LayoutParams"
textView3.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams(100, 100))
layout.addView(textView3)

我們看看 addView() 都做了什麼。

public void addView(View child) {
    addView(child, -1);
}

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
    if (mOrientation == HORIZONTAL) {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    } else if (mOrientation == VERTICAL) {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }
    return null;
}

public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
    }
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {

    // ...

    if (!checkLayoutParams(params)) {
        params = generateLayoutParams(params);
    }

    // ...
}

@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof LinearLayout.LayoutParams;
}

看起來 ViewGroup 真是煞費苦心,如果我們沒有給 View 設定 LayoutParams,則系統會幫我們根據 orientation 設定預設的 LayoutParams。甚至是我們即使在 addView() 之前設定了錯誤的 LayoutParams 值,系統也會我們幫我們進行糾正。

雖然系統已經做的足夠完善,幫我們各種矯正錯誤,但在 addView() 之後,我們還強行設定錯誤的 LayoutParams,那還是一定會報 ClassCastException 的。

LayoutParams 很重要,每一名 Android 開發都應該盡力地去掌握,只有弄清楚了系統的編寫方式,應對上面類似簡書的流式佈局才能更好處理。

實際上 Google 出的 FlexboxLayout 已經做的相當完美。
當然如果使用的 RecyclerView,還可以自己寫一個 FlowLayoutManager 進行處理。

原文較多地參考自:https://blog.csdn.net/yisizhu/article/details/51582