原文链接:https://blog.csdn.net/ldld1717/article/details/80458917
2. 自定义LayoutParams
回想一下我们平时使用RelativeLayout的时候,在布局文件中使用android:layout_alignParentRight="true"、android:layout_centerInParent="true"等各种属性,就能控制子控件显示在父控件的上下左右、居中等效果。
在View中有一个mLayoutParams的变量用来保存这个View的所有布局属性。ViewGroup.LayoutParams有两个属性layout_width和layout_height,因为所有的容器都需要设置子控件的宽高,所以这个LayoutParams是所有布局参数的基类,如果需要扩展其他属性,都应该继承自它。比如RelativeLayout中就提供了它自己的布局参数类RelativeLayout.LayoutParams,并扩展了很多布局参数,我们平时在RelativeLayout中使用的布局属性都来自它 :
看了上面的介绍,我们大概知道怎么为我们的布局容器定义自己的布局属性了吧,就不绕弯子了,按照下面的步骤做:
①. 大致明确布局容器的需求,初步定义布局属性
在定义属性之前要弄清楚,我们自定义的布局容器需要满足那些需求,需要哪些属性,比如,我们现在要实现像相对布局一样,为子控件设置一个位置属性layout_position=”“,来控制子控件在布局中显示的位置。暂定位置有五种:左上、左下、右上、右下、居中。有了需求,我们就在attr.xml定义自己的布局属性(和之前讲的自定义属性一样的操作,不太了解的可以翻阅 《深入解析自定义属性》。
left就代表是左上(按常理默认就是左上方开始,就不用写leftTop了,简洁一点),bottom左下,right 右上,rightAndBottom右下,center居中。属性类型是枚举,同时只能设置一个值。
②. 继承LayoutParams,定义布局参数类
我们可以选择继承ViewGroup.LayoutParams,这样的话我们的布局只是简单的支持layout_width和layout_height;也可以继承MarginLayoutParams,就能使用layout_marginxxx属性了。因为后面我们还要用到margin属性,所以这里方便起见就直接继承MarginLayoutParams了。
覆盖构造方法,然后在有AttributeSet参数的构造方法中初始化参数值,这个构造方法才是布局文件被映射为对象的时候被调用的。
public static class CustomLayoutParams extends MarginLayoutParams {
public static final int POSITION_MIDDLE = 0; // 中间
public static final int POSITION_LEFT = 1; // 左上方
public static final int POSITION_RIGHT = 2; // 右上方
public static final int POSITION_BOTTOM = 3; // 左下角
public static final int POSITION_RIGHTANDBOTTOM = 4; // 右下角
public int position = POSITION_LEFT; // 默认我们的位置就是左上角
public CustomLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs,R.styleable.CustomLayout );
//获取设置在子控件上的位置属性
position = a.getInt(R.styleable.CustomLayout_layout_position ,position );
a.recycle();
}
public CustomLayoutParams( int width, int height) {
super(width, height);
}
public CustomLayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
③. 重写generateLayoutParams()
在ViewGroup中有下面几个关于LayoutParams的方法,generateLayoutParams (AttributeSet attrs)是在布局文件被填充为对象的时候调用的,这个方法是下面几个方法中最重要的,如果不重写它,我么布局文件中设置的布局参数都不能拿到。后面我也会专门写一篇博客来介绍布局文件被添加到activity窗口的过程,里面会讲到这个方法被调用的来龙去脉。其他几个方法我们最好也能重写一下,将里面的LayoutParams换成我们自定义的CustomLayoutParams类,避免以后会遇到布局参数类型转换异常。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomLayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new CustomLayoutParams (p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new CustomLayoutParams (LayoutParams.MATCH_PARENT , LayoutParams.MATCH_PARENT);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof CustomLayoutParams ;
}
④. 在布局文件中使用布局属性
注意引入命名空间xmlns:openxu= "http://schemas.android.com/apk/res/包名"
⑤. 在onMeasure和onLayout中使用布局参数
经过上面几步之后,我们运行程序,就能获取子控件的布局参数了,在onMeasure方法和onLayout方法中,我们按照自定义布局容器的特殊需求,对宽度和位置坐特殊处理。这里我们需要注意一下,如果布局容器被设置为包裹类容,我们只需要保证能将最大的子控件包裹住就ok,代码注释比较详细,就不多说了。
@Override
protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
//获得此ViewGroup上级容器为其推荐的宽和高,以及计算模式
int widthMode = MeasureSpec. getMode(widthMeasureSpec);
int heightMode = MeasureSpec. getMode(heightMeasureSpec);
int sizeWidth = MeasureSpec. getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec. getSize(heightMeasureSpec);
int layoutWidth = 0;
int layoutHeight = 0;
// 计算出所有的childView的宽和高
measureChildren(widthMeasureSpec, heightMeasureSpec);
int cWidth = 0;
int cHeight = 0;
int count = getChildCount();
if(widthMode == MeasureSpec. EXACTLY){
//如果布局容器的宽度模式是确定的(具体的size或者match_parent),直接使用父窗体建议的宽度
layoutWidth = sizeWidth;
} else{
//如果是未指定或者wrap_content,我们都按照包裹内容做,宽度方向上只需要拿到所有子控件中宽度做大的作为布局宽度
for ( int i = 0; i < count; i++) {
View child = getChildAt(i);
cWidth = child.getMeasuredWidth();
//获取子控件最大宽度
layoutWidth = cWidth > layoutWidth ? cWidth : layoutWidth;
}
}
//高度很宽度处理思想一样
if(heightMode == MeasureSpec. EXACTLY){
layoutHeight = sizeHeight;
} else{
for ( int i = 0; i < count; i++) {
View child = getChildAt(i);
cHeight = child.getMeasuredHeight();
layoutHeight = cHeight > layoutHeight ? cHeight : layoutHeight;
}
}
// 测量并保存layout的宽高
setMeasuredDimension(layoutWidth, layoutHeight);
}
@Override
protected void onLayout( boolean changed, int left, int top, int right,
int bottom) {
final int count = getChildCount();
int childMeasureWidth = 0;
int childMeasureHeight = 0;
CustomLayoutParams params = null;
for ( int i = 0; i < count; i++) {
View child = getChildAt(i);
// 注意此处不能使用getWidth和getHeight,这两个方法必须在onLayout执行完,才能正确获取宽高
childMeasureWidth = child.getMeasuredWidth();
childMeasureHeight = child.getMeasuredHeight();
params = (CustomLayoutParams) child.getLayoutParams();
switch (params. position) {
case CustomLayoutParams. POSITION_MIDDLE: // 中间
left = (getWidth()-childMeasureWidth)/2;
top = (getHeight()-childMeasureHeight)/2;
break;
case CustomLayoutParams. POSITION_LEFT: // 左上方
left = 0;
top = 0;
break;
case CustomLayoutParams. POSITION_RIGHT: // 右上方
left = getWidth()-childMeasureWidth;
top = 0;
break;
case CustomLayoutParams. POSITION_BOTTOM: // 左下角
left = 0;
top = getHeight()-childMeasureHeight;
break;
case CustomLayoutParams. POSITION_RIGHTANDBOTTOM:// 右下角
left = getWidth()-childMeasureWidth;
top = getHeight()-childMeasureHeight;
break;
default:
break;
}
// 确定子控件的位置,四个参数分别代表(左上右下)点的坐标值
child.layout(left, top, left+childMeasureWidth, top+childMeasureHeight);
}
}
运行效果:
下面几个效果分别对应布局容器宽高设置不同的属性的情况(设置match_parent 、设置200dip、设置):
从运行结果看,我们自定义的布局容器在各种宽高设置下都能很好的测量大小和摆放子控件。现在我们让他支持margin属性
点击下方链接免费获取Android进阶资料:
https://shimo.im/docs/tXXKHgdjPYj6WT8d/