本文内容来自 Android开发必知50个诀窍 中的第三章
目标结果
放在自定义
CascadeLayout
里的 view 会出现这种叠加效果.本例是为了对 ViewGroup 的自定义流程,特别是其绘制流程有个认识,效果不是狂拽酷炫(本质还是我实力没到位).
<hp.com.nf.hp50.widget.CascadeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#44000000"
custom:horizontal_spacing="20dp"
custom:vertical_spacing="20dp">
<View
android:layout_width="100dp"
android:layout_height="150dp"
android:background="#ff0000"
/>
<View
android:layout_width="100dp"
android:layout_height="150dp"
android:background="#00ff00" />
<View
android:layout_width="100dp"
android:layout_height="150dp"
android:background="#0000ff" />
</hp.com.nf.hp50.widget.CascadeLayout>
1 理解 ViewGroup 的绘制流程
绘制布局由两个遍历过程组成:测量过程和布局过程。测量过程由measure(int, int)方法完成,该方法从上到下遍历视图树。在递归遍历过程中,每个视图都会向下层传递尺寸和规格。当measure方法遍历结束,每个视图都保存了各自的尺寸信息。
第二个过程由layout(int, int, int,int)方法完成,该方法也是由上而下遍历视图树,在遍历过程中,每个父视图通过测量过程的结果定位所有子视图的位置信息。”
1.1 关于measure
可以理解为
measure(int, int)
确定子 View 的大小和起始位置(左上角)
1.2关于layout(int, int, int,int)
layout(int, int, int,int)
就比较简单了,其中四个参数对应 left top right bottom
起始位置一个点可以确定 left top. 结合宽高 就可以得到 right = left+width. bottm = top+height
2 添加自定义属性名和默认值
在 values->attrs.xml 中添加自定义属性名
<declare-styleable name="CascadeLayout">
<attr name="horizontal_spacing" format="dimension" />
<attr name="vertical_spacing" format="dimension" />
</declare-styleable>在 values->dimes.xml 添加默认值
<dimen name="cascade_horizontal_spacing">10dp</dimen>
<dimen name="cascade_vertical_spacing">10dp</dimen>
3 自定义 CascadeLayout 继承 ViewGroup
这里把全部代码贴上
public class CascadeLayout2 extends ViewGroup {
private int mHorizontalSpacing;
private int mVerticalSpacing;
public CascadeLayout2(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CascadeLayout);
try {
mHorizontalSpacing = a.getDimensionPixelSize(
R.styleable.CascadeLayout_horizontal_spacing,
getResources().getDimensionPixelSize(
R.dimen.cascade_horizontal_spacing));
mVerticalSpacing = a.getDimensionPixelSize(
R.styleable.CascadeLayout_vertical_spacing, getResources()
.getDimensionPixelSize(R.dimen.cascade_vertical_spacing));
} finally {
a.recycle();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getPaddingLeft();
int height = getPaddingTop();
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//首先要让子 view 测量完毕
measureChild(child, widthMeasureSpec, heightMeasureSpec);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int l = getPaddingLeft() + mHorizontalSpacing * i;//left 坐标
int t = getPaddingTop() + mVerticalSpacing * i;//top 坐标
lp.x = l;
lp.y = t;
//保存 CascadeLayout 的宽高,支持padding
width = Math.max(width, l + child.getMeasuredWidth());
height = Math.max(height, t + child.getMeasuredHeight());
}
width += getPaddingRight();
height += getPaddingBottom();
setMeasuredDimension(resolveSize(width, widthMeasureSpec),
resolveSize(height, heightMeasureSpec));
// setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
+ child.getMeasuredHeight());
}
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p.width, p.height);
}
public static class LayoutParams extends ViewGroup.LayoutParams {
int x;
int y;
public LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LayoutParams(int w, int h) {
super(w, h);
}
}
}
3 代码解读
3.1 LayoutParams
内部类
此类为了存储各个子 View 的左上角坐标,因为继承了
ViewGroup.LayoutParams
,要求重写若干方法 例如checkLayoutParams
generateDefaultLayoutParams
generateLayoutParams
等.具体内容和父类差不多.当然可以用其他方式来实现保存子View的坐标,但是这种方法是最符合Google规范的
3.2 onMeasure 解读
3.2.1 确定子 view 的起点,保存到 LayoutParams 中
//遍历,子 View 的left定为到 mHorizontalSpacing * i
// getPaddingLeft() 是为了让 CascadeLayout 支持paddingleft属性,后面还有类似代码
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int l = getPaddingLeft() + mHorizontalSpacing * i;//left 坐标
int t = getPaddingTop() + mVerticalSpacing * i;//top 坐标
lp.x = l;
lp.y = t;
3.2.2 确定 CascadeLayout 的宽高
//保存 CascadeLayout 的宽高,支持padding,取子 View 所能达到的最大宽高
width = Math.max(width, l + child.getMeasuredWidth());
height = Math.max(height, t + child.getMeasuredHeight());
//CascadeLayout的测量,resolveSize方法 **保证测量模式有效**
setMeasuredDimension(resolveSize(width, widthMeasureSpec),
esolveSize(height, heightMeasureSpec));
当 CascadeLayout 是 wrap_content 属性时,效果如下(第一个最宽,第二个最长)
3.3 onLayout 解读
onLayout
就是最后一步了.遍历子 View 然后调用child.layout
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
+ child.getMeasuredHeight());
}