如何自定义ViewGroup

在开发中,有时会遇到一些需求,单单使用系统提供的布局和控件并不能够满足我们的开发,所以这个时候通常就可以自己定制布局(ViewGroup)和控件(View)了。我在这里,将会用一个最简单的例子,为大家解释一下自定义ViewGroup的基本流程,希望能够帮助到还不了解这个流程的朋友。

首先,我们想要实现的布局图如下:


就这样看起来十分简单,用系统提供的布局就可以实现了这个效果,根本不需要自定义ViewGroup来实现嘛!!!不过,如果你自己去尝试一下用系统的布局来做,你就会发现一些问题了。问题1是:要实现各个view错开的效果,就必须为他们设置不同的固定外边距参数,这样可能带来的问题就是:不同手机,显示效果可能会不一样,也就是适配问题!问题2是:如果想要加入的View多了,还需要自己计算每个View的外边距参数,很坑爹,再说,这里主要是讲解自定义ViewGroup的基本流程,所以,例子越简单,也就越好理解了!

首先,我们可以自定义自己ViewGroup想要的属性;这里我为ViewGroup定义了两个属性,horizontal_spacing(布局中的view的水平间距)和vertical_spacing(布局中的View的垂直间距);另外,还定义了一个布局参数的属性,layout_vertical_spacing,这个属性可以供我们自定义的ViewGroup中的View使用。

attrs.xml 的内容:

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

    <declare-styleable name="MyCustomLayout">
        <attr name="horizontal_spacing" format="dimension" />
        <attr name="vertical_spacing" format="dimension" />
    </declare-styleable>

    <declare-styleable name="MyCustomLayout_LayoutParams">
        <attr name="layout_vertical_spacing" format="dimension" />
    </declare-styleable>

</resources>

在dimens.xml中定义两个属性的默认值。

dimens.xml 的内容:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="horizontal_spacing">10dp</dimen>
    <dimen name="vertical_spacing">10dp</dimen>
</resources>

接着,最主要的步骤来了。先看看我们自定义的ViewGroup。

下面是源码:

package com.customlayout.mycustomlayout;

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


public class MyCustomLayout extends ViewGroup {

    private int mHorizontalSpacing;
    private int mVerticalSpacing;


    public MyCustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyCustomLayout);
        mHorizontalSpacing = ta.getDimensionPixelSize(R.styleable.MyCustomLayout_horizontal_spacing,
                getResources().getDimensionPixelSize(R.dimen.horizontal_spacing));
        mVerticalSpacing = ta.getDimensionPixelSize(R.styleable.MyCustomLayout_vertical_spacing,
                getResources().getDimensionPixelSize(R.dimen.vertical_spacing));
        ta.recycle();
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        // TODO Auto-generated method stub
        return p != null;
    }


    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        // TODO Auto-generated method stub
        return (LayoutParams) p;
    }


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


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


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = 0;
        int height = getPaddingTop();
        int verticalSpacing;
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            verticalSpacing = mVerticalSpacing;
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp.verticalSpacing > 0) {
                verticalSpacing += lp.verticalSpacing;
            }
            width = getPaddingLeft() + mHorizontalSpacing * i;
            lp.x = width;
            lp.y = height;
            width += child.getMeasuredWidth();
            height += verticalSpacing;
        }
        width += getPaddingRight();
        height += getChildAt(getChildCount() - 1).getMeasuredHeight() + getPaddingBottom();

        setMeasuredDimension(resolveSize(width, widthMeasureSpec),
                resolveSize(height, heightMeasureSpec));
    }

    @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());
        }
    }

    public static class LayoutParams extends ViewGroup.LayoutParams {
        public int x;
        public int y;
        public int verticalSpacing;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray ta = c.obtainStyledAttributes(attrs, R.styleable.MyCustomLayout_LayoutParams);
            verticalSpacing = ta.getDimensionPixelSize(R.styleable.MyCustomLayout_LayoutParams_layout_vertical_spacing, -1);
            ta.recycle();
        }

        public LayoutParams(int w, int h) {
            super(w, h);
        }
    }
}
首先,我们从MyCustomLayout.java 的构造方法说起。

public MyCustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyCustomLayout);
        mHorizontalSpacing = ta.getDimensionPixelSize(R.styleable.MyCustomLayout_horizontal_spacing,
                getResources().getDimensionPixelSize(R.dimen.horizontal_spacing));
        mVerticalSpacing = ta.getDimensionPixelSize(R.styleable.MyCustomLayout_vertical_spacing,
                getResources().getDimensionPixelSize(R.dimen.vertical_spacing));
        ta.recycle();
    }
这个构造方法有两个参数,其中attrs是我们布局的属性集合,有了这个参数,我们就可以获取到在布局文件xml中设置的相关属性了。

接着,讲onMeasure方法;顾名思义,这个方法是一个测量方法,它的作用是:遍历布局中的每一个View,对每一个View进行测量(调用measureChild方法),接着再为View设置LayoutParams,设置View的位置,即在屏幕上的x,y坐标,以便供onLayout方法中使用。这里的LayoutParams是我们重写的,其中int x和int y保存了布局中View的坐标位置。注意:重写LayoutParams类时,必须要对ViewGroup中的checkLayoutParams(ViewGroup.LayoutParams p)、generateLayoutParams(ViewGroup.LayoutParams p)、generateLayoutParams(AttributeSet attrs)、generateDefaultLayoutParams()进行重写,否则将会出现异常!!!

接下来,讲onLayout方法;这个方法是对布局中的全部View进行位置部署。从方法体中可以看到,它通过一个遍历,对布局中的每一个View调用layout方法进行位置部署。


好了,在这里稍微总结下:自定义ViewGroup流程中, 最主要是对onMeasure和onLayout两个方法进行重写。onMeasure通过遍历布局中的View,为每一个View测量了大小、计算布局参数等。onLayout则是通过遍历布局中的View,为每一个View进行位置布置。

在activity_main.xml 中使用我们自定义的ViewGroup:

<com.customlayout.mycustomlayout.MyCustomLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:customLayout="http://schemas.android.com/apk/res-auto"
    android:id="@+id/layout"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@android:color/white"
    customLayout:horizontal_spacing="65dp"
    customLayout:vertical_spacing="85dp">

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

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

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

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

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

</com.customlayout.mycustomlayout.MyCustomLayout>


MainActivity.java 的源码:

package com.customlayout.mycustomlayout;

import android.app.Activity;
import android.graphics.RectF;
import android.os.Bundle;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.BounceInterpolator;
import android.view.animation.LayoutAnimationController;
import android.view.animation.TranslateAnimation;

public class MainActivity extends Activity {

    MyCustomLayout layout;
    LayoutAnimationController layoutAnimationController;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        layout = (MyCustomLayout) findViewById(R.id.layout);

        AnimationSet set = new AnimationSet(true);

        AlphaAnimation a = new AlphaAnimation(0f, 1f);
        a.setDuration(500);
        TranslateAnimation t = new TranslateAnimation(Animation.RELATIVE_TO_SELF, -1f,
                Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f);
        t.setDuration(500);

        set.addAnimation(t);
        set.addAnimation(a);
        layoutAnimationController = new LayoutAnimationController(set);
        layout.setLayoutAnimation(layoutAnimationController);

    }
}

最后,讲一下布局动画;在ViewGroup类中,有一个属性是LayoutAnimation,也就是所谓的布局动画;只要我们指定一个动画给这个属性,那么ViewGroup中的每一个在布局时都能够带有动画效果。在上面的onCreate()方法中,我指定了一个动画集合并设置给了我自定义好的MyCustomLayout。运行一下程序,就可以看到,MyCustomLayout中的每一个View将会按顺序且带着我指定的动画出现在MyCustomLayout中,效果很酷!

结合这个简单的例子,给大家简单介绍了一下自定义ViewGroup的方法;在我们使用到的其他复杂布局中,都是采用我上面介绍的方法进行实现的,只是他们的计算将会比上面的例子复杂好多,但基本原理就还是这些。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值