【Android 开发技巧】如何自定义ViewGroup

来源:互联网 发布:双拇指扫矿软件 编辑:程序博客网 时间:2024/04/30 23:40

这篇文章准备说说如何自定义ViewGroup。对于新手来说,自定义ViewGroup是那种大牛级别的人物才能掌握的东西,自己却望而生畏。

不要怕,请谨记“一切新事物都是纸老虎,只要肯去花时间钻研,没有学不会的东西”。

废话不多说,接下来,就让我们来揭开自定义ViewGroup的神秘面纱。

依照惯例,先从一个例子说起。


很简单,3张扑克牌叠在一起显示。这个布局效果该如何实现呢?有的同学该说了,这很简单啊,用RelativeLayout或FrameLayout,然后为每一个扑克牌设置margin就能实现了。

ok,那就看一下通过这种方式是如何实现的。代码如下:

[html] view plaincopy在CODE上查看代码片派生到我的代码片
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="fill_parent"  
  3.     android:layout_height="fill_parent" >  
  4.   
  5.     <View  
  6.         android:layout_width="100dp"  
  7.         android:layout_height="150dp"  
  8.         android:background="#FF0000" />  
  9.   
  10.     <View  
  11.         android:layout_width="100dp"  
  12.         android:layout_height="150dp"  
  13.         android:layout_marginLeft="30dp"  
  14.         android:layout_marginTop="20dp"  
  15.         android:background="#00FF00" />  
  16.   
  17.     <View  
  18.         android:layout_width="100dp"  
  19.         android:layout_height="150dp"  
  20.         android:layout_marginLeft="60dp"  
  21.         android:layout_marginTop="40dp"  
  22.         android:background="#0000FF" />  
  23.   
  24. </RelativeLayout>  
效果图

没错,通过这种方式是可以实现的。但是,不觉得这种方式有点low吗?!让我们用高级一点的方式去实现它,提升一下自己的逼格!

定制ViewGroup之前,我们需要先理解几个概念。


Android绘制视图的方式
这里我不会涉及太多的细节,但是需要理解Android开发文档中的一段话:

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

简而言之,第一步是测量ViewGroup的宽度和高度,在onMeasure()方法中完成,ViewGroup遍历所有子视图计算出它的大小。第二步是根据第一步获取的尺寸去布局所有子视图,在onLayout()中完成。


创建CascadeLayout

终于到了定制ViewGroup的阶段了。假设我们已经定制了一个CascadeLayout的容器,我们会这样使用它。

[html] view plaincopy在CODE上查看代码片派生到我的代码片
  1. <FrameLayout xmlns:cascade="http://schemas.android.com/apk/res/com.manoel.custom"  
  2.     <!-- 声明命名空间 -->  
  3.     xmlns:android="http://schemas.android.com/apk/res/android"  
  4.     android:layout_width="fill_parent"  
  5.     android:layout_height="fill_parent" >  
  6.   
  7.     <com.manoel.view.CascadeLayout  
  8.         android:layout_width="fill_parent"  
  9.         android:layout_height="fill_parent"  
  10.         <!-- 自定义属性 -->  
  11.         cascade:horizontal_spacing="30dp"  
  12.         cascade:vertical_spacing="20dp" >  
  13.   
  14.         <View  
  15.             android:layout_width="100dp"  
  16.             android:layout_height="150dp"  
  17.             android:background="#FF0000" />  
  18.   
  19.         <View  
  20.             android:layout_width="100dp"  
  21.             android:layout_height="150dp"  
  22.             android:background="#00FF00" />  
  23.   
  24.         <View  
  25.             android:layout_width="100dp"  
  26.             android:layout_height="150dp"  
  27.             android:background="#0000FF" />  
  28.     </com.manoel.view.CascadeLayout>  
  29.   
  30. </FrameLayout>  

首先,定义属性。在values文件夹下面创建attrs.xml,代码如下:

[html] view plaincopy在CODE上查看代码片派生到我的代码片
  1. <resources>  
  2.     <declare-styleable name="CascadeLayout">  
  3.         <attr name="horizontal_spacing" format="dimension" />  
  4.         <attr name="vertical_spacing" format="dimension" />  
  5.     </declare-styleable>  
  6. </resources>  
同时,为了严谨一些,定义一些默认的垂直距离和水平距离,以防在布局中没有提供这些属性。

在dimens.xml中添加如下代码:

[html] view plaincopy在CODE上查看代码片派生到我的代码片
  1. <resources>  
  2.     <dimen name="cascade_horizontal_spacing">10dp</dimen>  
  3.     <dimen name="cascade_vertical_spacing">10dp</dimen>  
  4. </resources>  
准备工作已经做好了,接下来看一下CascadeLayout的源码,略微有点长,后面帮助大家分析一下。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class CascadeLayout extends ViewGroup {  
  2.   
  3.   private int mHorizontalSpacing;  
  4.   private int mVerticalSpacing;  
  5.   
  6.   public CascadeLayout(Context context, AttributeSet attrs) {  
  7.     super(context, attrs);  
  8.   
  9.     TypedArray a = context.obtainStyledAttributes(attrs,  
  10.         R.styleable.CascadeLayout);  
  11.   
  12.     try {  
  13.       mHorizontalSpacing = a.getDimensionPixelSize(  
  14.           R.styleable.CascadeLayout_horizontal_spacing,  
  15.           getResources().getDimensionPixelSize(  
  16.               R.dimen.cascade_horizontal_spacing));  
  17.   
  18.       mVerticalSpacing = a.getDimensionPixelSize(  
  19.           R.styleable.CascadeLayout_vertical_spacing, getResources()  
  20.               .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));  
  21.     } finally {  
  22.       a.recycle();  
  23.     }  
  24.   
  25.   }  
  26.   
  27.   @Override  
  28.   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  29.     int width = getPaddingLeft();  
  30.     int height = getPaddingTop();  
  31.     int verticalSpacing;  
  32.   
  33.     final int count = getChildCount();  
  34.     for (int i = 0; i < count; i++) {  
  35.       verticalSpacing = mVerticalSpacing;  
  36.   
  37.       View child = getChildAt(i);  
  38.       measureChild(child, widthMeasureSpec, heightMeasureSpec);  
  39.   
  40.       LayoutParams lp = (LayoutParams) child.getLayoutParams();  
  41.       width = getPaddingLeft() + mHorizontalSpacing * i;  
  42.   
  43.       lp.x = width;  
  44.       lp.y = height;  
  45.   
  46.       if (lp.verticalSpacing >= 0) {  
  47.         verticalSpacing = lp.verticalSpacing;  
  48.       }  
  49.   
  50.       width += child.getMeasuredWidth();  
  51.       height += verticalSpacing;  
  52.     }  
  53.   
  54.     width += getPaddingRight();  
  55.     height += getChildAt(getChildCount() - 1).getMeasuredHeight()  
  56.         + getPaddingBottom();  
  57.   
  58.     setMeasuredDimension(resolveSize(width, widthMeasureSpec),  
  59.         resolveSize(height, heightMeasureSpec));  
  60.   }  
  61.   
  62.   @Override  
  63.   protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  64.   
  65.     final int count = getChildCount();  
  66.     for (int i = 0; i < count; i++) {  
  67.       View child = getChildAt(i);  
  68.       LayoutParams lp = (LayoutParams) child.getLayoutParams();  
  69.   
  70.       child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y  
  71.           + child.getMeasuredHeight());  
  72.     }  
  73.   }  
  74.   
  75.   @Override  
  76.   protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {  
  77.     return p instanceof LayoutParams;  
  78.   }  
  79.   
  80.   @Override  
  81.   protected LayoutParams generateDefaultLayoutParams() {  
  82.     return new LayoutParams(LayoutParams.WRAP_CONTENT,  
  83.         LayoutParams.WRAP_CONTENT);  
  84.   }  
  85.   
  86.   @Override  
  87.   public LayoutParams generateLayoutParams(AttributeSet attrs) {  
  88.     return new LayoutParams(getContext(), attrs);  
  89.   }  
  90.   
  91.   @Override  
  92.   protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {  
  93.     return new LayoutParams(p.width, p.height);  
  94.   }  
  95.   
  96.   public static class LayoutParams extends ViewGroup.LayoutParams {  
  97.     int x;  
  98.     int y;  
  99.     public int verticalSpacing;  
  100.   
  101.     public LayoutParams(Context context, AttributeSet attrs) {  
  102.       super(context, attrs);  
  103.     }  
  104.   
  105.     public LayoutParams(int w, int h) {  
  106.       super(w, h);  
  107.     }  
  108.   
  109.   }  
  110. }  

首先,分析构造函数。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public CascadeLayout(Context context, AttributeSet attrs) {  
  2.     super(context, attrs);  
  3.   
  4.     TypedArray a = context.obtainStyledAttributes(attrs,  
  5.         R.styleable.CascadeLayout);  
  6.   
  7.     try {  
  8.       mHorizontalSpacing = a.getDimensionPixelSize(  
  9.           R.styleable.CascadeLayout_horizontal_spacing,  
  10.           getResources().getDimensionPixelSize(  
  11.               R.dimen.cascade_horizontal_spacing));  
  12.   
  13.       mVerticalSpacing = a.getDimensionPixelSize(  
  14.           R.styleable.CascadeLayout_vertical_spacing, getResources()  
  15.               .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));  
  16.     } finally {  
  17.       a.recycle();  
  18.     }  
  19.   
  20.   }  
如果在布局中使用CasecadeLayout,系统就会调用这个构造函数,这个大家都应该知道的吧。这里不解释why,有兴趣的可以去看源码,重点看系统是如何解析xml布局的。

构造函数很简单,就是通过布局文件中的属性,获取水平距离和垂直距离。


然后,分析自定义LayoutParams。

这个类的用途就是保存每个子视图的x,y轴位置。这里把它定义为静态内部类。ps:提到静态内部类,我又想起来关于多线程内存泄露的问题了,如果有时间再给大家解释一下多线程造成内存泄露的问题。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static class LayoutParams extends ViewGroup.LayoutParams {  
  2.     int x;  
  3.     int y;  
  4.     public int verticalSpacing;  
  5.   
  6.     public LayoutParams(Context context, AttributeSet attrs) {  
  7.       super(context, attrs);  
  8.     }  
  9.   
  10.     public LayoutParams(int w, int h) {  
  11.       super(w, h);  
  12.     }  
  13.   
  14.   }  

除此之外,还需要重写一些方法,checkLayoutParams()、generateDefaultLayoutParams()等,这个方法在不同ViewGroup之间往往是相同的。


接下来,分析onMeasure()方法。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. @Override  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.   int width = getPaddingLeft();  
  4.   int height = getPaddingTop();  
  5.   int verticalSpacing;  
  6.   
  7.   final int count = getChildCount();  
  8.   for (int i = 0; i < count; i++) {  
  9.     verticalSpacing = mVerticalSpacing;  
  10.   
  11.     View child = getChildAt(i);  
  12.     measureChild(child, widthMeasureSpec, heightMeasureSpec); // 令每个子视图测量自身  
  13.   
  14.     LayoutParams lp = (LayoutParams) child.getLayoutParams();  
  15.     width = getPaddingLeft() + mHorizontalSpacing * i;  
  16.     // 保存每个子视图的x,y轴坐标  
  17.     lp.x = width;  
  18.     lp.y = height;  
  19.   
  20.     if (lp.verticalSpacing >= 0) {  
  21.       verticalSpacing = lp.verticalSpacing;  
  22.     }  
  23.   
  24.     width += child.getMeasuredWidth();  
  25.     height += verticalSpacing;  
  26.   }  
  27.   
  28.   width += getPaddingRight();  
  29.   height += getChildAt(getChildCount() - 1).getMeasuredHeight()  
  30.       + getPaddingBottom();  
  31.   // 使用计算所得的宽和高设置整个布局的测量尺寸  
  32.   setMeasuredDimension(resolveSize(width, widthMeasureSpec),  
  33.       resolveSize(height, heightMeasureSpec));  
  34. }  


最后,分析onLayout()方法。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. @Override  
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  3.   
  4.   final int count = getChildCount();  
  5.   for (int i = 0; i < count; i++) {  
  6.     View child = getChildAt(i);  
  7.     LayoutParams lp = (LayoutParams) child.getLayoutParams();  
  8.   
  9.     child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y  
  10.         + child.getMeasuredHeight());  
  11.   }  
  12. }  
逻辑很简单,用onMeasure()方法计算出的值为参数循环调用子View的layout()方法。


为子视图添加自定义属性

作为示例,下面将添加子视图重写垂直间距的方法。

第一步是向attrs.xml中添加一个新的属性。

[html] view plaincopy在CODE上查看代码片派生到我的代码片
  1. <declare-styleable name="CascadeLayout_LayoutParams">  
  2.     <attr name="layout_vertical_spacing" format="dimension" />  
  3. </declare-styleable>  

这里的属性名是layout_vertical_spacing,因为该属性名前缀是layout_,同时,又不是View固有的属性,所以该属性会被添加到LayoutParams的属性表中。在CascadeLayout类的构造函数中读取这个新属性。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static class LayoutParams extends ViewGroup.LayoutParams {  
  2.     int x;  
  3.     int y;  
  4.     public int verticalSpacing;  
  5.   
  6.     public LayoutParams(Context context, AttributeSet attrs) {  
  7.       super(context, attrs);  
  8.   
  9.       TypedArray a = context.obtainStyledAttributes(attrs,  
  10.           R.styleable.CascadeLayout_LayoutParams);  
  11.       try {  
  12.         verticalSpacing = a  
  13.             .getDimensionPixelSize(  
  14.                 R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,  
  15.                 -1);  
  16.       } finally {  
  17.         a.recycle();  
  18.       }  
  19.     }  
  20.   
  21.     public LayoutParams(int w, int h) {  
  22.       super(w, h);  
  23.     }  
  24.   
  25.   }  

那怎么使用这个属性呢?so easy!

[html] view plaincopy在CODE上查看代码片派生到我的代码片
  1. <com.manoel.view.CascadeLayout  
  2.     android:layout_width="fill_parent"  
  3.     android:layout_height="fill_parent"  
  4.     cascade:horizontal_spacing="30dp"  
  5.     cascade:vertical_spacing="20dp" >  
  6.   
  7.     <!-- 注意:这张“扑克牌”使用了layout_vertical_spacing属性 -->  
  8.     <View  
  9.         android:layout_width="100dp"  
  10.         android:layout_height="150dp"  
  11.         cascade:layout_vertical_spacing="90dp"  
  12.         android:background="#FF0000" />  
  13.   
  14.     <View  
  15.         android:layout_width="100dp"  
  16.         android:layout_height="150dp"  
  17.         android:background="#00FF00" />  
  18.   
  19.     <View  
  20.         android:layout_width="100dp"  
  21.         android:layout_height="150dp"  
  22.         android:background="#0000FF" />  
  23. </com.manoel.view.CascadeLayout>  


参考资料

http://developer.android.com/guide/topics/ui/how-android-draws.html

http://developer.android.com/reference/android/view/ViewGroup.html

http://developer.android.com/reference/android/view/ViewGroup.LayoutParams.html 


后记

呼~这就是这篇文章所有的内容了。大家应该对如何自定义ViewGroup有了一个感性的认识了。

想要精通自定义ViewGroup,办法只有一个,那就是多动脑,勤动手!

好了,要说的就这么多,如果有什么问题,尽管留言,大家一起交流,一起进步!一起提升逼格,一起做UI大神

0 0