假设有下面一个布局文件
<?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_width和layout_height。这就是你可以在LinearLayout的子View中声明layout_width和layout_height属性的原因。
基本的布局参数
ViewGroup有两个最基本的布局参数类,ViewGroup.LayoutParams和ViewGroup.MarginLayoutParams。
所有的自定义ViewGroup的布局参数类必须要直接或者间接继承自ViewGroup.LayoutParams类,因为你需要支持layout_width和layout_height布局属性。
如果你想要让自定义ViewGroup的布局参数属性支持margin,例如layout_marginLeft,那么自定义ViewGroup的布局参数类需要直接或者间接继承自ViewGroup.MarginLayoutParams,因为它有解析margin属性的功能。
自定义布局参数
假设现在有一个自定义ViewGroup,名字叫做CornerLayout。CornerLayout会根据子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的左下角,如下图所示
自定义布局参数属性
首先需要为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有四个属性值,,名字分别为leftTop,rightTop,leftBottom,rightBottom,并且这个四个名字有对应的十六进制值。
定义了layout_corner属性后,就可以在XML布局参数中使用这个布局属性,不过前提是父View必须是CornerLayout。
创建布局参数类
现在要给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>
显示效果如下
实现动态添加View
到目前为止,我们只实现了在XML布局中为CornerLayout添加子View的功能,然而并没有实现动态添加子View功能。
动态添加子View是由ViewGroup的addView()这一类方法实现的。根据是否带有布局参数类型的参数,可以把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。
那么,CornerLayout的generateLayoutParams(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显示在右上角,并且rightMargin为30px,topMargin为40px。
代码参考
下面给出完整的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);
}
/**
* 定义布局参数类。
*/
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);
}
}
}