导航

技巧三 自定义的ViewGroup

Posted on 2013-09-04 08:40  Sharp陈响  阅读(700)  评论(2编辑  收藏  举报

本文翻译自《50 Android Hacks》个人感觉很不错的一本书,简单部分我就不翻了,或者留到最后翻,现在先翻一些最值得跟大家分享的部分。

技巧三 自定义的ViewGroup

  适用Android v1.6+

  当我们设计程序的时候,可能需要创建一些复杂的Views,我们希望在不同的activity中展示这些Views。比如一个扑克游戏,我们需要作出3.1的效果。怎么实现呢?

 

图3.1

  一种实现方式是使用RelativeLayout通过给成员设置不同的margin来实现上面的效果:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayoutxmlns: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>

 

结果如图3.2.

 

图3.2

  在这个技巧中,我们将学习如果使用自定义ViewGroup的方式实现这种布局。这种方法比xml实现的好处在于:

     1.更容易重复使用,减少代码量(谢谢菊花同学)

     2.可以通过自定义属性的方式自定义ViewGroup的位置。

     3.Xml会更加简洁,因此也更加容易理解。

     4.如果你要改变margin不用自己去重新计算每个成员的margin值。

So~Go~

  3.1 理解如何绘制Views

  如果要自定义ViewGroup我们需要理解Android是如何绘制View的。这里不会深入讲解,但是我们需要理解3.5小节的内容,因为它解释了如何绘制一个layout。

  绘制一个layout是一个两步走的过程:measure,layout(我感觉这里不翻译更好,翻译了反而容易误解)。Measure在函数measure(int, int)中实现,是一个自上而下的遍历调用,每个成员view都保存着自己的尺寸参数。第二个过程在layout(int, int, int,int)函数中实现,也是一个自上而下的遍历过程。在这个过程中每个父成员负责使用在measure(int, int)中计算得到的尺寸给成员view布局。

  为了理解这个概念,我们需要分析一下ViewGroup的绘制过程。首先计算ViewGroup的宽度(width)和高度(height),这个过程在onMeasure()函数中进行。在这个方法中ViewGroup会通过计算其成员的高度和宽度来它自己的宽度和高度。我们通过onLayout()函数来完成第二步,在第二步中,ViewGroup会通过使用onMeasure()中得到的值来绘制其子成员。

  3.2创建层叠布局

  在这一部分中,我们将会编写自定义ViewGroup起名叫CascadeLayout,将会得到如图3.2的结果。配置该ViewGroup的XML代码如下:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:cascade="http://schemas.android.com/apk/res/com.manning.androidhacks.hack003"
    android:layout_width="fill_parent" android:layout_height="fill_parent">

    <com.manning.androidhacks.hack003.view.CascadeLayout
        android:layout_width="fill_parent" android:layout_height="fill_parent"
        cascade:horizontal_spacing="30dp" cascade: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" />

    </com.manning.androidhacks.hack003.view.CascadeLayout>
    
</FrameLayout>

 

  现在你知道我们需要怎么样构建了(我知道毛啊),让我们开始吧。第一件事:我们需要在res/values中创建一个attrs.xml文件,来自定义这些属性。代码如下:

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

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

 

  有时候用户不输入一些属性的值,所以这里我们还需要定义属性的默认值,这些默认值是放在 res/values目录下的 dimens.xml文件中。代码如下:

 

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

 

  在我们理解了安卓是如何绘制views之后您可能在想我们需要一个继承自ViewGroup 的类CascadeLayout,然后重写他的onMeasure()和onLayout()方法.由于代码比较长我们分三块来分析他:构造, onMeasure()方法和onLayout()方法.下面是构造方法部分:

public class CascadeLayout extends ViewGroup {

    private int mHorizontalSpacing;
    private int mVerticalSpacing;

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

 

  

  译者注:

       a.getDimensionPixelSize(R.styleable.CascadeLayout_vertical_spacing,getResources().getDimensionPixelSize(R.dimen.cascade_vertical_spacing));

       R.styleable.CascadeLayout_vertical_spacing为所要获取的属性名

            getResources().getDimensionPixelSize(R.dimen.cascade_vertical_spacing)为默认值

        getDimensionPixelSize 为获取其pix的值

  在编写onMeasure()方法之前,我们首先创建一个自定义的LayoutParams类。这个类会保存每个子成员的x,y坐标的位置。这个自定义的LayoutParams类会被当做CascadeLayout的内部类,这个类以如下方式定义:

 

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

  我们还需要重写其他方法:checkLayoutParams(),generateDefaultLayoutParams(),generateLayoutParams(AttributeSet attrs)和generateLayoutParams(ViewGroup.LayoutParams p)。这几个方法在不同的ViewGroups的实现千篇一律,你可以很轻松的找到这些代码。

  接下来是onMeasure()方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //用xy来记录最终ViewGroup的大小
    int width = 0;
    int height = getPaddingTop();
    final int count = getChildCount();
    
    for (int i = 0; i < count; i++) {
         View child = getChildAt(i);
         //计算没个子成员的大小并放入widthMeasureSpec和heightMeasureSpec中
         measureChild(child, widthMeasureSpec, heightMeasureSpec);
         //获得子成员的布局参数,其中x和y为左上点的坐标
         LayoutParams lp = (LayoutParams) child.getLayoutParams();
         //得到需要设置子成员的左padding
         width = getPaddingLeft() + mHorizontalSpacing * i;
         //设置,设置左上坐标
         lp.x = width;
         lp.y = height;
         //左上坐标+实际宽度,得到ViewGroup的大小
         width += child.getMeasuredWidth();
         //这里只是计算空白
         height += mVerticalSpacing;
    }
    
    //加上右padding得到总宽度
    width += getPaddingRight();
    //TopPadding+ getMeasuredHeight(子成员的实际高度)+BottomPdding得到总高度
    height += getChildAt(getChildCount() - 1).getMeasuredHeight()+ getPaddingBottom();
    setMeasuredDimension(resolveSize(width, widthMeasureSpec),  resolveSize(height, heightMeasureSpec));
}

 

     注:resolveSize根据所选模式取得,想要的大小。此函数如下:

public static int resolveSize(int size, int measureSpec) {
         int result = size;
         int specMode = MeasureSpec.getMode(measureSpec);
         int specSize =  MeasureSpec.getSize(measureSpec);

         switch (specMode) {
         case MeasureSpec.UNSPECIFIED:
             result = size;
             break;
         case MeasureSpec.AT_MOST:
             result = Math.min(size, specSize);
             break;
         case MeasureSpec.EXACTLY:
             result = specSize;
             break;
         }
         return result;
     }

 

    这段代码的解释可以参照http://www.cnblogs.com/franksunny/archive/2012/04/20/2459738.html

    中的介绍,但是他的内容有些许问题。

    最后是onLayout():

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

    注:onLayout(boolean changed, int left, int top, int right, int bottom)

    具体内容参考

    http://www.cnblogs.com/franksunny/archive/2012/04/20/2459738.html

    此文不赘述。

    这代码太他妈简单了(他说的)。So,还是解释一下吧,谁让我是菜鸟。根据每个child的x,y重新对child进行布局。

    3.3 给子成员增加自定义属性

    在这个部分,我们将研究一下怎么给成员view增加自定义属性。举个栗子,增加一个子成员在竖直方向上与父view左边距和右边距(设置不均匀的排列)。如图3.3.

 

图3.3

    同样第一件事我们需要在attrs.xml中增加相应的属性:

<declare-styleable name="CascadeLayout_LayoutParams">

    <attr name="layout_vertical_spacing" format="dimension" />

</declare-styleable>

    因为这个属性名以layout_开头,所以它被增加到LayoutParams的属性中,我们需要把这个属性增加到LayoutParams的构造函数中就像我们给CascadeLayout增加的属性那样,代码如下:

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

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

 

    verticalSpacing是一个公共字段,我们在CascadeLayout的onMeasure()方法中使用他,如果子View的LayoutParams(应该是attrs)包含verticalSpacing属性,我们就可以使用他。代码如下:

verticalSpacing = mVerticalSpacing;

。。。。。。。。

LayoutParams lp = (LayoutParams) child.getLayoutParams();

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

。。。。。。

width += child.getMeasuredWidth();

height += verticalSpacing;

    问题:强转会不会调用构造函数?

3.4 最主要的

    使用自定义的Views和ViewGroups是一个组织app中各种布局非常好的方法。自定义组件同样会提供自定义的动作。如果你希望建立一个复杂的布局,可以考虑是否需要自定义一个ViewGroups。这是一个磨刀不误砍柴工的过程。开始工作量稍大,但是会让开发更顺畅。