Android LayoutParams詳解

提示:本文的源碼均取自Android 7.0

前言

在平時的開發過程中,我們一般是通過XML文件去定義佈局,所以對於LayoutParams的使用可能相對較少。但是在需要動態改變View的佈局參數(比如寬度、位置)時,就必須要藉助這個重要的類了。本文將結合具體源碼詳細講解LayoutParams的相關知識。

基礎知識

LayoutParams是什麼

LayoutParams翻譯過來就是佈局參數,子View通過LayoutParams告訴父容器(ViewGroup)應該如何放置自己。從這個定義中也可以看出來LayoutParams與ViewGroup是息息相關的,因此脫離ViewGroup談LayoutParams是沒有意義的。

事實上,每個ViewGroup的子類都有自己對應的LayoutParams類,典型的如LinearLayout.LayoutParams和FrameLayout.LayoutParams等,可以看出來LayoutParams都是對應ViewGroup子類的內部類。

最基礎的LayoutParams是定義在ViewGroup中的靜態內部類,封裝着View的寬度和高度信息,對應着XML中的layout_widthlayout_height屬性。主要源碼如下:

public static class LayoutParams {
    public static final int FILL_PARENT = -1;
    public static final int MATCH_PARENT = -1;
    public static final int WRAP_CONTENT = -2;

    public int width;
    public int height;

    ......

    /**
     * XML文件中設置的以layout_開頭的屬性將在這個方法中解析
     */
    public LayoutParams(Context c, AttributeSet attrs) {
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
        // 解析width和height屬性
        setBaseAttributes(a,
                R.styleable.ViewGroup_Layout_layout_width,
                R.styleable.ViewGroup_Layout_layout_height);
        a.recycle();
    }

    /**
     * 使用傳入的width和height構建LayoutParams
     */
    public LayoutParams(int width, int height) {
        this.width = width;
        this.height = height;
    }

    /**
     * 通過傳入的LayoutParams構建新的LayoutParams
     */
    public LayoutParams(LayoutParams source) {
        this.width = source.width;
        this.height = source.height;
    }
    ......
}

弄清楚了LayoutParams的意義,就可以解釋爲什麼在XML中View的某些屬性是以layout_開頭的了。因爲這些屬性並不直接屬於View,而是屬於這些View的LayoutParams,這樣的命名方式也就顯得很貼切了。

MarginLayoutParams

在ViewGroup中還定義一個LayoutParams的子類——MarginLayoutParams。從名字就可以猜出來,MarginLayoutParams是和外間距有關的。事實也確實如此,和LayoutParams相比,MarginLayoutParams只是增加了對上下左右外間距的支持。實際上大部分LayoutParams的實現類都是繼承自MarginLayoutParams,因爲基本所有的父容器都是支持子View設置外間距的。MarginLayoutParams的主要源碼如下:

public static class MarginLayoutParams extends ViewGroup.LayoutParams {
    /**
     * The left margin in pixels of the child. Margin values should be positive.
     * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
     * to this field.
     */
    public int leftMargin;

    public int topMargin;

    public int rightMargin;

    public int bottomMargin;

    /**
     * 解析XML中以layout_開頭的屬性
     */
    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);
            int verticalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);

            if (horizontalMargin >= 0) {
                leftMargin = horizontalMargin;
                rightMargin = horizontalMargin;
            } else {
                leftMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
                        UNDEFINED_MARGIN);
                rightMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginRight,
                        UNDEFINED_MARGIN);            
            }
            .........
        a.recycle();
    }
}

從源碼中也可以看到,MarginLayoutParams主要就是增加了上下左右4種外間距。在構造方法中,先是獲取了margin屬性;如果該值不合法,就獲取horizontalMargin;如果該值不合法,再去獲取leftMargin和rightMargin屬性(verticalMargin、topMargin和bottomMargin同理)。我們可以據此總結出這幾種屬性的優先級:

margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin

優先級更高的屬性會覆蓋掉優先級較低的屬性。此外,還要注意一下這幾種屬性上的註釋:

Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value

也就是說,如果我們更改了MarginLayoutParams中這幾種屬性的值,就應該調用View的setLayoutParams方法重新設置更改後的MarginLayoutParams,這樣我們所做的更改纔會生效(其實主要是因爲在setLayoutParams方法中調用了requestLayout方法)。

LayoutParams與View如何建立聯繫

說了這麼多LayoutParams的作用,這裏再簡單談一下LayoutParams是何時被創建出來的,又是怎樣和View建立聯繫。歸納起來,View的使用方式無非有兩種:在XML中定義View在Java代碼中直接生成View對應的實例對象,因此我們也分這兩個方向進行探索。

在Java代碼中實例化View

在代碼中實例化View後,如果調用setLayoutParams方法爲View設置指定的LayoutParams,那麼LayoutParams就已經和View建立起聯繫了。針對不同的ViewGroup子類,我們要選擇合適的LayoutParams。

實例化View後,一般還會調用addView方法將View對象添加到指定的ViewGroup中。可以想到,在ViewGroup中肯定也會爲還沒有LayoutParams的子View設置合適的LayoutParams,下文將通過分析代碼說明這一過程。ViewGroup實現了以下五種addView方法的重載版本:

/**
 * 重載方法1:添加一個子View
 * 如果這個子View還沒有LayoutParams,就爲子View設置當前ViewGroup默認的LayoutParams
 */
public void addView(View child) {
    addView(child, -1);
}

/**
 * 重載方法2:在指定位置添加一個子View
 * 如果這個子View還沒有LayoutParams,就爲子View設置當前ViewGroup默認的LayoutParams
 * @param index View將在ViewGroup中被添加的位置(-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();// 生成當前ViewGroup默認的LayoutParams
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

/**
 * 重載方法3:添加一個子View
 * 使用當前ViewGroup默認的LayoutParams,並以傳入參數作爲LayoutParams的width和height
 */
public void addView(View child, int width, int height) {
    final LayoutParams params = generateDefaultLayoutParams();  // 生成當前ViewGroup默認的LayoutParams
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}

/**
 * 重載方法4:添加一個子View,並使用傳入的LayoutParams
 */
@Override
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}

/**
 * 重載方法4:在指定位置添加一個子View,並使用傳入的LayoutParams
 */
public void addView(View child, int index, LayoutParams params) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

以上代碼已經添加了必要的註釋,這裏就不再贅述了。總之,只要子View沒有LayoutParams,ViewGroup就會爲其設置默認的LayoutParams。默認的LayoutParams對象通過generateDefaultLayoutParams方法生成,ViewGroup中的代碼實現如下:

protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

實際上,addView的前四個重載方法最終都會調用第五個重載版本,即addView(View child, int index, LayoutParams params)。在這個方法中調用了requestLayoutinvalidate方法,引起視圖重新佈局(onMeasure->onLayout->onDraw)和重繪。這也很好理解,既然我們添加了新的View,那麼原有的視圖結構自然會發生變化。同時,在這個方法中還調用了addViewInner方法,關鍵代碼如下:

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
    .....
    if (mTransition != null) {
        mTransition.addChild(this, child);
    }

    if (!checkLayoutParams(params)) { // ① 檢查傳入的LayoutParams是否合法
        params = generateLayoutParams(params); // 如果傳入的LayoutParams不合法,將進行轉化操作
    }

    if (preventRequestLayout) { // ② 是否需要阻止重新執行佈局流程
        child.mLayoutParams = params; // 這不會引起子View重新佈局(onMeasure->onLayout->onDraw)
    } else {
        child.setLayoutParams(params); // 這會引起子View重新佈局(onMeasure->onLayout->onDraw)
    }

    if (index < 0) {
        index = mChildrenCount;
    }

    addInArray(child, index);

    // tell our children
    if (preventRequestLayout) {
        child.assignParent(this);
    } else {
        child.mParent = this;
    }
    .....
}

可以看到,在代碼①的位置先判斷傳入的LayoutParams是否合法,ViewGroup中這個方法只是簡單判斷了傳入的LayoutParams是否爲空:

protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return  p != null;
}

如果LayoutParams不合法,將使用generateLayoutParams方法對其進行轉化,ViewGroup中這個方法僅僅將傳入的LayoutParams原樣返回:

protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return p;
}

最後,在代碼②的位置爲子View設置LayoutParams。這裏分爲了兩種情況:如果不希望引起子View重新佈局(onMeasure->onLayout->onDraw)就直接爲子View的LayoutParams變量賦值;否則調用子View的setLayoutParams方法傳入LayoutParams。

到這一步,LayoutParams和View的聯繫就建立起來了。

在XML中定義View

在XML中定義的View首先會被解析爲對應的實例化對象,這項工作將通過LayoutInflaterinflate方法完成。inflater方法有多個重載版本,最終將會調用inflate(XmlPullParser parser,ViewGroup root, boolean attachToRoot),關鍵代碼如下:

/**
 * 解析XML文件中的View
 * @param parser 解析器
 * @param root 父容器(可能爲null)
 * @param attachToRoot View是否需要附加到父容器中
 */
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    ......
    View result = root;

    ......
    final String name = parser.getName();

    if (TAG_MERGE.equals(name)) { // 針對<merge>標籤
        ......
    } else { // 針對普通標籤
        // ① 通過XML生成對應的View對象
        // Temp指的是XML文件中的根View
        final View temp = createViewFromTag(root, name, inflaterContext, attrs); 

        ViewGroup.LayoutParams params = null;

        if (root != null) {
            // ② 通過XML中的佈局參數生成對應的LayoutParams
            params = root.generateLayoutParams(attrs); 
            if (!attachToRoot) {
                // ③ 如果不需要將View附加到父容器中,就直接爲View設置LayoutParams
                temp.setLayoutParams(params);
            }
        }

        rInflateChildren(parser, temp, attrs, true); // 解析View中包含的子View(如果存在的話)

        // ④ 如果父容器不爲null,且需要將View附加到父容器中,就使用addView方法
        if (root != null && attachToRoot) {
            root.addView(temp, params);
        }

        // Decide whether to return the root that was passed in or the top view found in xml.
        if (root == null || !attachToRoot) {
            result = temp;
        }
    }
    ......
    return result;
}

可以看到,如果父容器(ViewGroup)不爲空,在代碼②的位置將通過父容器的generateLayoutParams方法生成LayoutParams,這也間接說明了LayoutParams是與ViewGroup息息相關的,脫離ViewGroup談LayoutParams是沒有意義的。

在代碼③的位置,如果attachToRoot參數爲false,代表不需要將View添加到父容器中,那就直接爲View設置LayoutParams;否則在代碼④的位置通過addView(temp, params)將View添加到父容器中。到了這一步,後續邏輯就和在Java代碼中實例化View是一樣的了。

其實最典型的例子就是在Activity中調用setContentView方法,系統會通過LayoutInflater將整個XML文件解析爲View Tree,從根佈局開始爲每個View和ViewGroup設置相應的LayoutParams。

自定義LayoutParams

如果我們需要自定義ViewGroup的話,一般也會自定義LayoutParams,這樣可以提供一些個性化的佈局參數。爲了支持設置外間距,自定義的LayoutParams一般會選擇繼承ViewGroup.MarginLayoutParams。此外,還需要在XML文件中定義declare-styleable資源屬性,一般會創建一個名爲attrs.xml文件放置這些屬性。這裏假設我們要實現一個名爲SimpleViewGroup的自定義ViewGroup,示例代碼如下:

<resources>
    <declare-styleable name="SimpleViewGroup_Layout">
        <!-- 自定義的屬性 -->
        <attr name="layout_simple_attr" format="integer"/>
        <!-- 使用系統預置的屬性 -->
        <attr name="android:layout_gravity"/>
    </declare-styleable>
</resources>

這裏將declare-styleable的name設置爲SimpleViewGroup_Layout,也就是自定義ViewGroup的名稱加上_Layout。這裏一共定義了兩個屬性,第一個屬性使用了自定義的名稱,需要提供nameformat參數,format用於限制自定義屬性的類型;第二個屬性使用了系統預置的屬性,比如這裏的android:layout_gravity,好處是可以讓用戶使用熟悉的屬性(在系統提供的屬性語義合適時可以考慮這種方式)。不過要注意,這種情況下就不要爲它定義format參數了,因爲系統已經設置好了。

之後,需要在自定義的LayoutParams中解析這些屬性,下面是一個簡單的示例:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    public int simpleAttr;
    public int gravity;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        // 解析佈局屬性
        TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
        simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
        gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);

        typedArray.recycle();//釋放資源
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    public LayoutParams(MarginLayoutParams source) {
        super(source);
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}

最後,我們還需要重寫ViewGroup中幾個與LayoutParams相關的方法,示例代碼如下:

// 檢查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 
    return p instanceof SimpleViewGroup.LayoutParams;
}

// 生成默認的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 
    return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}

// 對傳入的LayoutParams進行轉化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 
    return new SimpleViewGroup.LayoutParams(p);
}

// 對傳入的LayoutParams進行轉化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 
    return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}

這些方法的作用已經在前文介紹過了,同時代碼中也添加了註釋,這裏就不再贅述了。

LayoutParams常見的子類

在爲View設置LayoutParams的時候需要根據它的父容器選擇對應的LayoutParams,否則結果可能與預期不一致,這裏簡單羅列一些常見的LayoutParams子類:

  • ViewGroup.MarginLayoutParams
  • FrameLayout.LayoutParams
  • LinearLayout.LayoutParams
  • RelativeLayout.LayoutParams
  • RecyclerView.LayoutParams
  • GridLayoutManager.LayoutParams
  • StaggeredGridLayoutManager.LayoutParams
  • ViewPager.LayoutParams
  • WindowManager.LayoutParams

參考資料

https://blog.csdn.net/yisizhu/article/details/51582622

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章