如何给自定义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);
        }
    }

}
复制代码
相关文章
相关标签/搜索