你的位置:首页 > 操作系统

[操作系统]如何自定义ViewGroup


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

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

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

 

[html] view plaincopy在CODE上查看代码片派生到我的代码片 


  1. <RelativeLayout "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 "http://schemas.android.com/apk/res/com.manoel.custom"  
  2.     <!-- 声明命名空间 -->  
  3.     "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.

 

 

[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.

 

[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,有兴趣的可以去看源码,重点看系统是如何解析

 

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

 

然后,分析自定义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.

 

[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