3、如何自定义ViewGroup

来源:互联网 发布:线切割编程入门图 编辑:程序博客网 时间:2024/04/30 19:37

转载的地址:http://blog.csdn.net/manoel/article/details/39062737

本文翻译自《50 android hacks》


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


很简单,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.      // 这里面是将dp转化为px显示
  14.       mHorizontalSpacing = a.getDimensionPixelSize(  
  15.           R.styleable.CascadeLayout_horizontal_spacing,  
  16.           getResources().getDimensionPixelSize(  
  17.               R.dimen.cascade_horizontal_spacing));  
  18.   
  19.       mVerticalSpacing = a.getDimensionPixelSize(  
  20.           R.styleable.CascadeLayout_vertical_spacing, getResources()  
  21.               .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));  
  22.     } finally {  
  23.       a.recycle();  
  24.     }  
  25.   
  26.   }  
  27.   
  28.   @Override  
  29.   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  30.    // 这里面的onMeasure方法参考文章:http://blog.csdn.net/a396901990/article/details/36475213
  31.     int width = getPaddingLeft();  
  32.     int height = getPaddingTop();  
  33.     int verticalSpacing;  
  34.   
  35.     final int count = getChildCount();  
  36.     for (int i = 0; i < count; i++) {  
  37.       verticalSpacing = mVerticalSpacing;  
  38.   
  39.       View child = getChildAt(i);  
  40.       measureChild(child, widthMeasureSpec, heightMeasureSpec);  
  41.   
  42.       LayoutParams lp = (LayoutParams) child.getLayoutParams();  
  43.       width = getPaddingLeft() + mHorizontalSpacing * i;  
  44.   
  45.       lp.x = width;  
  46.       lp.y = height;  
  47.   
  48.       if (lp.verticalSpacing >= 0) {  
  49.         verticalSpacing = lp.verticalSpacing;  
  50.       }  
  51.   
  52.       width += child.getMeasuredWidth();  
  53.       height += verticalSpacing;  
  54.     }  
  55.   
  56.     width += getPaddingRight();  
  57.     height += getChildAt(getChildCount() - 1).getMeasuredHeight()  
  58.         + getPaddingBottom();  
  59.   
  60.     setMeasuredDimension(resolveSize(width, widthMeasureSpec),  
  61.         resolveSize(height, heightMeasureSpec));  
  62.   }  
  63.   
  64.   @Override  
  65.   protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  66.   
  67.     final int count = getChildCount();  
  68.     for (int i = 0; i < count; i++) {  
  69.       View child = getChildAt(i);  
  70.       LayoutParams lp = (LayoutParams) child.getLayoutParams();  
  71.   
  72.       child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y  
  73.           + child.getMeasuredHeight());  
  74.     }  
  75.   }  
  76.   
  77.   @Override  
  78.   protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {  
  79.     return p instanceof LayoutParams;  
  80.   }  
  81.   
  82.   @Override  
  83.   protected LayoutParams generateDefaultLayoutParams() {  
  84.     return new LayoutParams(LayoutParams.WRAP_CONTENT,  
  85.         LayoutParams.WRAP_CONTENT);  
  86.   }  
  87.   
  88.   @Override  
  89.   public LayoutParams generateLayoutParams(AttributeSet attrs) {  
  90.     return new LayoutParams(getContext(), attrs);  
  91.   }  
  92.   
  93.   @Override  
  94.   protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {  
  95.     return new LayoutParams(p.width, p.height);  
  96.   }  
  97.   
  98.   public static class LayoutParams extends ViewGroup.LayoutParams {  
  99.     int x;  
  100.     int y;  
  101.     public int verticalSpacing;  
  102.   
  103.     public LayoutParams(Context context, AttributeSet attrs) {  
  104.       super(context, attrs);  
  105.     }  
  106.   
  107.     public LayoutParams(int w, int h) {  
  108.       super(w, h);  
  109.     }  
  110.   
  111.   }  
  112. }  

首先,分析构造函数。

[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 


0 0