自定义 ViewGroup 实现子 View 层叠效果

本文内容来自 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());
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值