自定义viewGroup

问题:如何创建一个如下图所示的布局?

  • \
    图1
    (原文地址:http://blog.csdn.net/vector_yi/article/details/24415537)
    你可能会说,利用RelativeLayout和margins就可以实现。的确,如下XML代码可以简单地构建一个类似的布局:
    01. <RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
    02. android:layout_width= "fill_parent"
    03. android:layout_height= "fill_parent" >
    04.  
    05. <View
    06. android:layout_width ="100dp"
    07. android:layout_height ="150dp"
    08. android:background ="#FF0000" />
    09.  
    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>
    效果如图2:
    \
    图2
    但是当遇到复杂、要求可变的类似布局时,利用margins可能就会显得操作很繁杂。 在此,我们来看另一种创建类似上图布局的方式---自定义ViewGroup 好处有以下几点:
  • 当你将这个布局应用到不同Activity中时更加容易维护
  • 可以利用自定义属性来自定义ViewGroup中的每个子View
  • 更加简洁可读的XML文件内容
  • 如果需要改变margin的时候,不需要手动的去计算每个子View的margin 一、理解Android绘制一个View的步骤 关于绘制View的步骤,可以参见Android官方文档:http://developer.android.com/guide/topics/ui/how-android-draws.html 在此,我们重点来关注ViewGroup的绘制过程: 1.处理ViewGroup的width和height. 处理width及height的操作在onMeasure()方法中进行,在此方法内,ViewGroup会根据它的子View来计算自身所占用的布局空间。 2.布局到页面上 这点操作在onLayout()方法中进行,在此方法中,ViewGroup会根据从onMeasure()中得到的信息将其每一个子View绘制出来。 
    二、构建CascadeLayout类 首先在XML布局文件中添加CascadeLayout:
    01. <FrameLayout
    02. <!--自定义命名空间,以便在下文中使用自定义的属性-->
    05. android:layout_width= "fill_parent"
    06. android:layout_height= "fill_parent" >
    07.  
    08. <com.manning.androidhacks.hack003.view.CascadeLayout
    09. android:layout_width ="fill_parent"
    10. android:layout_height ="fill_parent"
    11. cascade:horizontal_spacing ="30dp"<!--因为前面添加了cascade命名空间,所以此处可以使用自定义属性-->
    12. cascade:vertical_spacing ="20dp" >
    13.  
    14. <View
    15. android:layout_width ="100dp"
    16. android:layout_height ="150dp"
    17. cascade:layout_vertical_spacing ="90dp"<!--为子View添加的自定义属性,将在本文第三部分用到-->
    18. android:background ="#FF0000" />
    19.  
    20. <View
    21. android:layout_width ="100dp"
    22. android:layout_height ="150dp"
    23. android:background ="#00FF00" />
    24.  
    25. <View
    26. android:layout_width ="100dp"
    27. android:layout_height ="150dp"
    28. android:background ="#0000FF" />
    29. </com.manning.androidhacks.hack003.view.CascadeLayout>
    30.  
    31. </FrameLayout>
    要使用这些自定义的属性,我们必须要定义它。 在res/values文件夹下创建一个attrs.xml文件:
    1. <? xml version ="1.0" encoding= "utf-8" ?>
    2. <resources>
    3. <declare-styleable name= "CascadeLayout" >
    4. <attr name= "horizontal_spacing" format = "dimension" />
    5. <attr name= "vertical_spacing" format = "dimension" />
    6. </declare-styleable>
    7. </resources>
    然后,当我们在创建CascadeLayout且没有为其指定horizontal_spacing与vertical_spacing时,需要有一个默认值。 我们将这个默认值预先定义好并存放在res/values文件夹下的dimens.xml中:
    1. <? xml version ="1.0" encoding= "utf-8" ?>
    2. <resources>
    3. <dimen name= "cascade_horizontal_spacing" >10dp</dimen>
    4. <dimen name= "cascade_vertical_spacing" >10dp</dimen>
    5. </resources>
    最后,我们需要创建一个名为CascadeLayout的Java类,它继承了ViewGroup并重写了onMeasure()与OnLayout()方法。 1.CascadeLayout的构造函数
    01. public CascadeLayout (Context context, AttributeSet attrs) {
    02. super( context, attrs);
    03.  
    04. TypedArray a = context .obtainStyledAttributes (attrs ,
    05. R. styleable. CascadeLayout );
    06.  
    07. try {
    08. mHorizontalSpacing = a. getDimensionPixelSize(
    09. 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. }
    2.构建自定义的LayoutParams类 LayoutParams类将作为CascadeLayout的内部类存在,它将存储每个子View的x,y坐标。定义如下:
    01. public static class LayoutParams extends ViewGroup .LayoutParams {
    02. int x;
    03. int y;
    04.  
    05. public LayoutParams( Context context , AttributeSet attrs) {
    06. super (context , attrs );
    07. }
    08.  
    09. public LayoutParams( int w , int h ) {
    10. super (w , h );
    11. }
    12.  
    13. }

    3.重写onMeasure()方法 onMeasure()方法将是CascadeLayout类中最关键的部分,这个方法不仅计算整个ViewGroup所占用的布局空间,还将计算出每个子View所占用的布局空间。
    01. @Override
    02. protected void onMeasure (int widthMeasureSpec , int heightMeasureSpec ) {
    03. int width = 0;
    04. int height = getPaddingTop ();
    05.  
    06. final int count = getChildCount ();
    07. for int i = 0; i < count; i++) {
    08. View child = getChildAt (i );
    09. measureChild (child , widthMeasureSpec , heightMeasureSpec );
    10. LayoutParams lp = (LayoutParams ) child .getLayoutParams ();
    11. width = getPaddingLeft () + mHorizontalSpacing * i;
    12.  
    13. lp .x = width;
    14. lp .y = height;
    15.  
    16. width += child .getMeasuredWidth ();
    17. height += mVerticalSpacing ;
    18. }
    19.  
    20. width += getPaddingRight ();
    21. height += getChildAt (getChildCount () - 1). getMeasuredHeight ()
    22. + getPaddingBottom ();
    23.  
    24. setMeasuredDimension ( resolveSize( width, widthMeasureSpec ),
    25. resolveSize( height, heightMeasureSpec ));
    26. }

    4.最后一步,重写onLayout()方法 代码很简单,就是让每个子View都调用layout()方法。
    01. @Override
    02. protected void onLayout (boolean changed, int l , int t , int r , int b ) {
    03.  
    04. final int count = getChildCount ();
    05. for int i = 0; i < count; i++) {
    06. View child = getChildAt (i );
    07. LayoutParams lp = ( LayoutParams ) child .getLayoutParams ();
    08.  
    09. child .layout (lp .x , lp .y , lp .x + child. getMeasuredWidth (), lp .y
    10. + child .getMeasuredHeight ());
    11. }
    12. }

    至此,就利用自定义的ViewGroup创建了一个和图2一样效果的布局页面。 
    三、为子View添加自定义属性 
    既然费了这么大劲,怎么可能就和之前几行XML代码效果一样? 下面,我们就来为CascadeLayout中的子View添加自定义属性: 首先,在之前创建的attrs.xml中添加如下代码: 
    1. <declare-styleable name="CascadeLayout_LayoutParams">
    2. <attr name="layout_vertical_spacing" format="dimension" />
    3. </declare-styleable>
    因为这个新添加的属性是以 layout_ 开头的,所以它会被添加到LayoutParams中去。 我们可以在之前自定义的内部类LayoutParams中的构造函数中读取到这个属性,将第一个构造函数改为:
    01. public LayoutParams (Context context, AttributeSet attrs) {
    02. super (context , attrs );
    03.  
    04. TypedArray a = context .obtainStyledAttributes (attrs ,
    05. R. styleable. CascadeLayout_LayoutParams );
    06. try {
    07. verticalSpacing = a
    08. .getDimensionPixelSize (
    09. R .styleable .CascadeLayout_LayoutParams_layout_vertical_spacing ,
    10. -1 );
    11. finally {
    12. a .recycle ();
    13. }
    14. }

    既然添加了新的自定义属性,就必须在onMeasure()方法中对其加以处理:
    01. @Override
    02. protected void onMeasure (int widthMeasureSpec , int heightMeasureSpec ) {
    03. int width = getPaddingLeft ();
    04. int height = getPaddingTop ();
    05. int verticalSpacing ;
    06.  
    07. final int count = getChildCount ();
    08. for int i = 0; i < count; i++) {
    09. 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.  
    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. }

    \

    最后附上完整的CascadeLayout代码: 
    001. package com.manning.androidhacks.hack003.view;
    002.  
    003. import android.content.Context;
    004. import android.content.res.TypedArray;
    005. import android.util.AttributeSet;
    006. import android.view.View;
    007. import android.view.ViewGroup;
    008.  
    009. import com.manning.androidhacks.hack003.R;
    010.  
    011. public class CascadeLayout extends ViewGroup {
    012.  
    013. private int mHorizontalSpacing;
    014. private int mVerticalSpacing;
    015.  
    016. public CascadeLayout(Context context, AttributeSet attrs) {
    017. super(context, attrs);
    018.  
    019. TypedArray a = context.obtainStyledAttributes(attrs,
    020. R.styleable.CascadeLayout);
    021.  
    022. try {
    023. mHorizontalSpacing = a.getDimensionPixelSize(
    024. R.styleable.CascadeLayout_horizontal_spacing,
    025. getResources().getDimensionPixelSize(
    026. R.dimen.cascade_horizontal_spacing));
    027.  
    028. mVerticalSpacing = a.getDimensionPixelSize(
    029. R.styleable.CascadeLayout_vertical_spacing, getResources()
    030. .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));
    031. finally {
    032. a.recycle();
    033. }
    034.  
    035. }
    036.  
    037. @Override
    038. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    039. int width = getPaddingLeft();
    040. int height = getPaddingTop();
    041. int verticalSpacing;
    042.  
    043. final int count = getChildCount();
    044. for (int i = 0; i < count; i++) {
    045. verticalSpacing = mVerticalSpacing;
    046.  
    047. View child = getChildAt(i);
    048. measureChild(child, widthMeasureSpec, heightMeasureSpec);
    049.  
    050. LayoutParams lp = (LayoutParams) child.getLayoutParams();
    051. width = getPaddingLeft() + mHorizontalSpacing * i;
    052.  
    053. lp.x = width;
    054. lp.y = height;
    055.  
    056. if (lp.verticalSpacing >= 0) {
    057. verticalSpacing = lp.verticalSpacing;
    058. }
    059.  
    060. width += child.getMeasuredWidth();
    061. height += verticalSpacing;
    062. }
    063.  
    064. width += getPaddingRight();
    065. height += getChildAt(getChildCount() - 1).getMeasuredHeight()
    066. + getPaddingBottom();
    067.  
    068. setMeasuredDimension(resolveSize(width, widthMeasureSpec),
    069. resolveSize(height, heightMeasureSpec));
    070. }
    071.  
    072. @Override
    073. protected void onLayout(boolean changed, int l, int t, int r, int b) {
    074.  
    075. final int count = getChildCount();
    076. for (int i = 0; i < count; i++) {
    077. View child = getChildAt(i);
    078. LayoutParams lp = (LayoutParams) child.getLayoutParams();
    079.  
    080. child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
    081. + child.getMeasuredHeight());
    082. }
    083. }
    084.  
    085. @Override
    086. protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    087. return instanceof LayoutParams;
    088. }
    089.  
    090. @Override
    091. protected LayoutParams generateDefaultLayoutParams() {
    092. return new LayoutParams(LayoutParams.WRAP_CONTENT,
    093. LayoutParams.WRAP_CONTENT);
    094. }
    095.  
    096. @Override
    097. public LayoutParams generateLayoutParams(AttributeSet attrs) {
    098. return new LayoutParams(getContext(), attrs);
    099. }
    100.  
    101. @Override
    102. protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    103. return new LayoutParams(p.width, p.height);
    104. }
    105.  
    106. public static class LayoutParams extends ViewGroup.LayoutParams {
    107. int x;
    108. int y;
    109. public int verticalSpacing;
    110.  
    111. public LayoutParams(Context context, AttributeSet attrs) {
    112. super(context, attrs);
    113.  
    114. TypedArray a = context.obtainStyledAttributes(attrs,
    115. R.styleable.CascadeLayout_LayoutParams);
    116. try {
    117. verticalSpacing = a
    118. .getDimensionPixelSize(
    119. R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,
    120. -1);
    121. finally {
    122. a.recycle();
    123. }
    124. }
    125.  
    126. public LayoutParams(int w, int h) {
    127. super(w, h);
    128. }
    129.  
    130. }
    131. }

    工程目录结构:


    (原文地址:http://blog.csdn.net/vector_yi/article/details/24415537)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值