zoukankan      html  css  js  c++  java
  • 如何自定义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>  

    参考资料

  • 相关阅读:
    HttpWebRequest中的ContentType详解
    c# 创建Windows服务
    转载 Url编码
    在应用程序级别之外使用注册为 allowDefinition='MachineToApplication' 的节是错误的。
    IE兼容模式下 SCRIPT1028: 缺少标识符、字符串或数字
    Response.ContentLength获取文件大小
    unable to instantiate activity...
    查看android-support-v4.jar引出的问题
    导入项目 R.java没有
    初识python: 局部变量、全局变量
  • 原文地址:https://www.cnblogs.com/joey-hua/p/4741625.html
Copyright © 2011-2022 走看看