本文翻译自《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。这是一个磨刀不误砍柴工的过程。开始工作量稍大,但是会让开发更顺畅。