创建定制的ViewGroup

如需建如下的布局,我们应该如何创建这样的布局呢?

这里写图片描述

虽然使用margin属性便足以实现这种布局,XML布局文件源码如下:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent">

    <View
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:background="#FF0000"/>

    <View
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:layout_marginLeft="30dp"
        android:layout_marginTop="20dp"
        android:background="#00FF00"/>

    <View
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:layout_marginLeft="60dp"
        android:layout_marginTop="40dp"
        android:background="#0000FF"/>

</RelativeLayout>

但我们可以通过创建自定义的ViewGroup来实现上述功能。该方法相对于在XML文件中手工指定margin值有如下优点:
1、在不同Activity中复用该视图时,更易维护。
2、开发者可以使用自定义属性来定制ViewGroup中子视图的位置。
3、布局文件更简明,更容易理解。
4、如果需要修改margin,不必重新手动计算每个子视图的margin。

理解Android绘制视图的方式

Android如何绘制一个布局:
”绘制布局由两个遍历过程组成:测量过程和布局过程。测量过程由measure(int, int)方法完成,该方法从上到下遍历视图树。在递归遍历过程中,每个视图都会向下层传递尺寸和规格。当measure方法遍历结束,每个视图都保存了各自的尺寸信息。第二个过程由layout(int, int, int, int)方法完成,该方法也是从上到下遍历视图树,在遍历过程中,每个父视图通过测量过程的结果定位所有子视图的位置信息“。

为了理解这个概念,下面分析ViewGroup的绘制过程。第一步是测量ViewGroup的宽度和高度。在onMeasure()方法中完成这步操作。在该方法中,ViewGroup通过遍历所有子视图计算出它的大小。最后一步操作,在onLayout()方法中完成,在该方法中,ViewGroup利用上一步计算出的测量信息,布局所有子视图。

创建自定义CascadeLayout

首先要定义CascadeLayout的定制属性,需要在res/values目录下创建一个属性文件attrs.xml,该文件的内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--为CascadeLayout视图添加自定义属性。
    declare-styleable中的name属性值必为自定义控件的类名(CascadeLayout),不然在布局文件中将无法使自定义控件的定制属性-->
    <declare-styleable name="CascadeLayout">
        <attr name="horizontal_spacing" format="dimension"/>
        <attr name="vertical_spacing" format="dimension"/>
    </declare-styleable>

    <!--为CascadeLayout的子视图添加自定义属性。添加为特定子视图重写(override)垂直间距的方法,name属性值必为父控件类名_父控件的LayoutParams内部类(CascadeLayout_LayoutParams)-->
    <declare-styleable name="CascadeLayout_LayoutParams">
        <!--因为属性名的前缀是layout_,没有包含一个视图属性,因此该属性会被添加到LayoutParams的属性表中-->
        <attr name="layout_vertical_spacing" format="dimension"/>
    </declare-styleable>
</resources>

同时还需要指定水平间距和垂直间距的默认值,以便在未指定这些值时使用。把这些默认值保存在dimens.xml文件中,该文件同样位于res/values文件夹下。dimens.xml文件如下:

<resources>   
    <dimen name="cascade_horizontal_spacing">10dp</dimen>
    <dimen name="cascade_vertical_spacing">10dp</dimen>
</resources>

创建CascadeLayout类

package com.example.huangfei.demo;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by huangfeihong on 2015/10/25.
 */
public class CascadeLayout extends ViewGroup {

    private int mHorizontalSpacing;//水平间距
    private int mVerticalSpacing;//垂直间距

    /**
     * 当通过XML文件创建该视图的实例时会调用该构造函数
     */
    public CascadeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout);

        try {
            /**
             * mHorizontalSpacing和mVerticalSpacing由自定义属性中获取,如果其值未指定,就使用默认值
             */
            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) {
        //使用宽和高计算布局的最终大小以及子视图的x与y轴位置
        int width = getPaddingLeft();
        int height = getPaddingTop();
        int verticalSpacing;//子视图垂直间距

        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            verticalSpacing = mVerticalSpacing;

            View child = getChildAt(i);
            //令每个子视图测量自身
            measureChild(child, widthMeasureSpec, heightMeasureSpec);

            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            width = getPaddingLeft() + mHorizontalSpacing * i;

            //在LayoutParams中保存每个子视图的x和y坐标
            lp.x = width;
            lp.y = height;

            if(lp.verticalSpacing >= 0){
                verticalSpacing = lp.verticalSpacing;
            }

            width += child.getMeasuredWidth();
            height += verticalSpacing;
        }

        width += getPaddingRight();
        height += getChildAt(count - 1).getMeasuredHeight() + getPaddingBottom();

        //使用计算所得的宽和高设置整个布局的测量尺寸
        setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
    }

    /**
     * 该方法以onMeasure()计算出的值为参数循环调用子View的layout()方法
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        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());
        }
    }


    /**
     * 要使自新定义的CascadeLayout.LayoutParams类,必须重写以下四个方法。
     * 这些方法的代码在不同ViewGroup之间往往是相同的。
     */
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p.width, p.height);
    }

    /**
     * 该类用于保存每个子视图的x、y轴位置
     */
    public static class LayoutParams extends ViewGroup.LayoutParams {
        int x;
        int y;
        int verticalSpacing;//子视图自定义垂直间距

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CascadeLayout_LayoutParams);

            try {
                verticalSpacing = a.getDimensionPixelSize(
                        R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing, -1);
            } finally {
                a.recycle();
            }
        }

        public LayoutParams(int w, int h) {
            super(w, h);
        }

    }
}

创建Activity的布局文件

<?xml version="1.0" encoding="utf-8"?>
<!--在XML中使用自定义属性时指定自定义命名空间-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:cascade="http://schemas.android.com/apk/res-auto"
             android:layout_width="fill_parent"
             android:layout_height="fill_parent">

    <!--通过cascade命名空间,就可以使用其自定义属性-->
    <com.example.huangfei.demo.CascadeLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        cascade:horizontal_spacing="50dp"
        cascade:vertical_spacing="20dp">
        <!--父视图添加自定义属性-->


        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:background="#FF0000"
            cascade:layout_vertical_spacing="90dp"/>
        <!--子视图添加自定义属性-->

        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:background="#00FF00"
            cascade:layout_vertical_spacing="50dp"/>

        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:background="#0000FF"/>
    </com.example.huangfei.demo.CascadeLayout>

</FrameLayout>

代码地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值