每日一問: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/51582622


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM