如何給自定義ViewGroup實現LayoutParams

假設有下面一個佈局文件php

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">

    <TextView android:layout_width="100dp" android:layout_height="100dp" android:text="Hello World!" />

</LinearLayout>
複製代碼

系統在加載這個佈局的時候,會建立一個LinearLayout對象和一個TextView對象,而後會調用LinearLayout.addView()方法保存這個TextView對象,同時也會建立LinearLayout.LayoutParams對象來保存TextView所聲明的佈局參數,例如layout_widthlayout_height。這就是你能夠在LinearLayout的子View中聲明layout_widthlayout_height屬性的緣由。java

基本的佈局參數

ViewGroup有兩個最基本的佈局參數類,ViewGroup.LayoutParamsViewGroup.MarginLayoutParamsandroid

全部的自定義ViewGroup的佈局參數類必需要直接或者間接繼承自ViewGroup.LayoutParams類,由於你須要支持layout_widthlayout_height佈局屬性。app

若是你想要讓自定義ViewGroup的佈局參數屬性支持margin,例如layout_marginLeft,那麼自定義ViewGroup的佈局參數類須要直接或者間接繼承自ViewGroup.MarginLayoutParams,由於它有解析margin屬性的功能。ide

自定義佈局參數

假設如今有一個自定義ViewGroup,名字叫作CornerLayoutCornerLayout會根據子View的layou_corner佈局屬性決定子View放置在哪一個角落。例如如今有下面一個佈局函數

<?xml version="1.0" encoding="utf-8"?>
<com.bxll.layoutparamsdemo.CornerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent">

    <View android:layout_width="100dp" android:layout_height="100dp" android:background="@color/colorAccent" app:layout_corner="leftBottom" />

</com.bxll.layoutparamsdemo.CornerLayout>
複製代碼

layout_corner的值爲leftBottom,所以這個View會顯示在CornerLayout的左下角,以下圖所示佈局

leftBottom

自定義佈局參數屬性

首先須要爲CornerLayout建立一個自定義的layout_corner屬性測試

<declare-styleable name="CornerLayout_Layout">
        <attr name="layout_corner" format="flags" >
            <flag name="leftTop" value="0x01" />
            <flag name="rightTop" value="0x02" />
            <flag name="leftBottom" value="0x04" />
            <flag name="rightBottom" value="0x08" />
        </attr>
    </declare-styleable>
複製代碼

能夠看出layout_corner有四個屬性值,,名字分別爲leftToprightTopleftBottomrightBottom,而且這個四個名字有對應的十六進制值。this

定義了layout_corner屬性後,就能夠在XML佈局參數中使用這個佈局屬性,不過前提是父View必須是CornerLayoutspa

建立佈局參數類

如今要給CornerLayout建立一個佈局參數類,用來解析layout_corner屬性,我把它命名爲CornerLayoutParams。因爲我須要CornerLayoutParams支持margin特性,所以我選擇讓它繼承自ViewGroup.MarginLayoutParams

public class CornerLayout extends ViewGroup {
    // 定義佈局參數類
    public static class CornerLayoutParams extends ViewGroup.MarginLayoutParams {
        // 這些常量須要與layout_corner的屬性值相對應
        public static final int CORNER_LEFT_TOP = 0x01;
        public static final int CORNER_RIGHT_TOP = 0x02;
        public static final int CORNER_LEFT_BOTTOM = 0x04;
        public static final int CORNER_RIGHT_BOTTOM = 0x08;

        public int mCorner = CORNER_LEFT_TOP;

        public CornerLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CornerLayout_Layout);
            mCorner = a.getInt(R.styleable.CornerLayout_Layout_layout_corner, CORNER_LEFT_TOP);
            a.recycle();
        }
    }
}
複製代碼

這裏,我只建立了一個構造函數CornerLayoutParams(Context c, AttributeSet attrs),而且在這個構造函數中解析出layout_corner屬性值。

其實還有還有不少構造函數,而這一個構造函數是必須的,由於系統在解析佈局文件的時候,會調用這個構造函數來建立佈局參數。

實現接口

那麼問題來了,系統怎麼精確的知道是建立CornerLayout.CornerLayoutParams對象而不是建立其餘的佈局參數類的對象,例如LinearLayout.LayoutParams。固然是複寫某個系統的接口,這個接口就是ViewGroup類的generateLayoutParams(AttributeSet attrs)方法。

所以在CornerLayout中複寫這個接口來建立CornerLayout.CornerLayoutParams對象。

@Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new CornerLayoutParams(getContext(), attrs);
    }
複製代碼

如此一來就能夠在XML佈局中快樂的使用layout_corner佈局屬性了,並且也可使用margin這一類屬性。例以下面一個佈局就使用了這兩個佈局屬性

<?xml version="1.0" encoding="utf-8"?>
<com.umx.layoutparamsdemo.CornerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">

    <View android:layout_width="100dp" android:layout_height="100dp" android:layout_marginEnd="10dp" android:layout_marginBottom="10dp" android:background="@color/colorAccent" app:layout_corner="rightBottom" />

</com.umx.layoutparamsdemo.CornerLayout>
複製代碼

顯示效果以下

right_bottom_with_margin

實現動態添加View

到目前爲止,咱們只實現了在XML佈局中爲CornerLayout添加子View的功能,然而並無實現動態添加子View功能。

動態添加子View是由ViewGroupaddView()這一類方法實現的。根據是否帶有佈局參數類型的參數,能夠把addView()分爲兩類,以下。

// 不帶有佈局參數類型參數的方法
public void addView(View child) {}
public void addView(View child, int index) {}
public void addView(View child, int width, int height) {}

// 帶有佈局參數類型參數的方法
public void addView(View child, LayoutParams params) {}
public void addView(View child, int index, LayoutParams params) {}
複製代碼

若是你使用的是不帶佈局參數類型參數的addView()方法,系統須要建立一個佈局參數對象。以addView(View child, int index)爲例來分析下

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);
    }
複製代碼

能夠看到,若是添加的子View沒有佈局參數對象,那麼就會調用generateDefaultLayoutParams()方法來建立一個默認的佈局參數對象。

所以CornerLayout若是須要支持動態添加View的特性,那麼還須要複寫generateDefaultLayoutParams()方法,在這個方法中須要提供最基本的寬高屬性,以及mCorner的默認值。

public class CornerLayout extends ViewGroup {
    
    /** * 系統建立默認佈局參數的接口。 */
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new CornerLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }    

    /** * 定義佈局參數類。 */
    public static class CornerLayoutParams extends ViewGroup.MarginLayoutParams {
        public int mCorner = CORNER_LEFT_TOP;

        public CornerLayoutParams(int width, int height) {
            super(width, height);
            // 因爲mCorner有默認值,所以這裏再也不提供默認值
        }
    }

}
複製代碼

無論addView()方法是否帶有佈局參數類型的參數,最終都會調用addViewInner()方法來實現,而addViewInner()方法根據狀況來校訂佈局參數類型的對象

private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) {
        // ...
        
        // 若是佈局參數對象不合法就須要校訂對象
        if (!checkLayoutParams(params)) {
            params = generateLayoutParams(params);
        }
        
        // ...
    }
複製代碼

checkLayoutParams()方法用來檢測佈局參數類型的對象是否合法,對於CornerLayout來講,須要檢測這個對象的類型是否爲CornerLayoutParams,所以實現以下

/** * 檢測參數類型是否合法。 */
    @Override
    protected boolean checkLayoutParams(LayoutParams p) {
        return p instanceof CornerLayoutParams;
    }
複製代碼

若是佈局參數對象不合法,那麼就須要調用generateLayoutParams(LayoutParams p)來校訂,而校訂的方式是抽取須要的佈局屬性,例如layout_widht, layout_height,以及margin

那麼,CornerLayoutgenerateLayoutParams(LayoutParams p)的實現以下

public class CornerLayout extends ViewGroup {

    /** * 根據不合法的參數p, 從新建立CornerLayoutParams對象。 */
    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        if (p instanceof MarginLayoutParams) {
            return new CornerLayoutParams((MarginLayoutParams)p);
        }
        return new CornerLayoutParams(p);
    }

    /** * 定義佈局參數。 */
    public static class CornerLayoutParams extends ViewGroup.MarginLayoutParams {
        public int mCorner = CORNER_LEFT_TOP;

        public CornerLayoutParams(MarginLayoutParams source) {
            // 調用父類構造函數解析layout_width, layout_height, margin屬性
            super(source);
            // mCorner有默認值,所以這裏不須要再提供默認值
        }

        public CornerLayoutParams(LayoutParams source) {
            // 調用父類構造函數,解析layout_width和layout_heigth屬性
            super(source);
            // mCorner有默認值,所以這裏不須要再提供默認值
        }
    }

}
複製代碼

至此,咱們已經爲CornerLayout實現了動態添加子View的功能。

測試動態添加子View功能

假設MainActivity.java加載的佈局以下

<?xml version="1.0" encoding="utf-8"?>
<com.umx.layoutparamsdemo.CornerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/corner_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

</com.umx.layoutparamsdemo.CornerLayout>
複製代碼

很簡單,就一個CornerLayout佈局。如今咱們到代碼中動態建立一個View對象,並添加到CornerLayout

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CornerLayout cornerLayout = findViewById(R.id.corner_layout);
        // 建立一個紅色背景的View對象
        View view = new View(this);
        view.setBackgroundColor(getResources().getColor(R.color.colorAccent));
        // 建立佈局參數對象
        CornerLayout.CornerLayoutParams layoutParams =
                new CornerLayout.CornerLayoutParams(300, 300);
        // 設置layout_corner值
        layoutParams.mCorner = CornerLayout.CornerLayoutParams.CORNER_RIGHT_TOP;
        // 設置margin值
        layoutParams.rightMargin = 30;
        layoutParams.topMargin = 40;
        // 設置佈局參數
        view.setLayoutParams(layoutParams);
        // 添加到CornerLayout中
        cornerLayout.addView(view);
    }
}
複製代碼

代碼中建立了一個View對象,而且設置了layout_corner以及margin屬性,最後把這個View對象添加到CornerLayout佈局中。實際效果就是View顯示在右上角,而且rightMargin30pxtopMargin40px

代碼參考

下面給出完整的CornerLayout代碼,以供參考

public class CornerLayout extends ViewGroup {
    public CornerLayout(Context context) {
        this(context, null);
    }

    public CornerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            // 只對第一個子View進行layout
            View first = getChildAt(0);
            CornerLayoutParams layoutParams = (CornerLayoutParams) first.getLayoutParams();
            int left;
            int top;
            switch (layoutParams.mCorner) {
                case CornerLayoutParams.CORNER_LEFT_TOP:
                    left = getPaddingLeft() + layoutParams.leftMargin;
                    top = getPaddingTop() + layoutParams.topMargin;
                    break;

                case CornerLayoutParams.CORNER_RIGHT_TOP:
                    top = getPaddingTop() + layoutParams.topMargin;
                    left = getWidth() - getPaddingRight() - first.getMeasuredWidth() - layoutParams.rightMargin;
                    break;

                case CornerLayoutParams.CORNER_LEFT_BOTTOM:
                    top = getHeight() - getPaddingBottom() - first.getMeasuredHeight() - layoutParams.bottomMargin;
                    left = getPaddingLeft() + layoutParams.leftMargin;
                    break;

                case CornerLayoutParams.CORNER_RIGHT_BOTTOM:
                    top = getHeight() - getPaddingBottom() - layoutParams.bottomMargin - first.getMeasuredHeight();
                    left = getWidth() - getPaddingRight() - layoutParams.rightMargin - first.getMeasuredWidth();
                    break;

                default:
                    left = getPaddingLeft() + layoutParams.leftMargin;
                    top = getPaddingTop() + layoutParams.topMargin;
            }
            first.layout(left, top, left + first.getMeasuredWidth(), top + first.getMeasuredHeight());
        }
    }

    /** * 系統建立佈局參數的接口 */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new CornerLayoutParams(getContext(), attrs);
    }

    /** * 系統建立默認佈局參數的接口。 */
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new CornerLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    /** * 檢測參數類型是否合法。 */
    @Override
    protected boolean checkLayoutParams(LayoutParams p) {
        return p instanceof CornerLayoutParams;
    }

    /** * 根據不合法的參數p, 從新建立CornerLayoutParams對象。 */
    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        if (p instanceof MarginLayoutParams) {
            return new CornerLayoutParams((MarginLayoutParams)p);
        }
        return new CornerLayoutParams(p);
    }

    /** *&emsp;定義佈局參數類。 */
    public static class CornerLayoutParams extends ViewGroup.MarginLayoutParams {
        public static final int CORNER_LEFT_TOP = 0x01;
        public static final int CORNER_RIGHT_TOP = 0x02;
        public static final int CORNER_LEFT_BOTTOM = 0x04;
        public static final int CORNER_RIGHT_BOTTOM = 0x08;

        public int mCorner = CORNER_LEFT_TOP;

        public CornerLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CornerLayout_Layout);
            mCorner = a.getInt(R.styleable.CornerLayout_Layout_layout_corner, CORNER_LEFT_TOP);
            a.recycle();
        }

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

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

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

}
複製代碼
相關文章
相關標籤/搜索