Hack3--Creating a custom ViewGroup

假设现在要创建一个如下图所示的布局,你会怎么做?


一种简单做法是利用margin 属性,代码如下:

<RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
  android:layout_width= "fill_parent"
  android:layout_height= "fill_parent" >

  <View
    android:layout_width ="100dp"
    android:layout_height ="150dp"
    android:background ="#FF0000" />

  <View
    android:layout_width ="100dp"
    android:layout_height ="150dp"
    android:layout_marginLeft ="30dp"
    android:layout_marginTop ="20dp"
    android:background ="#00FF00" />

  <View
    android:layout_width ="100dp"
    android:layout_height ="150dp"
    android:layout_marginLeft ="60dp"
    android:layout_marginTop ="40dp"
    android:background ="#0000FF" />

</RelativeLayout>
另一种做法是;将这三个view放进一个自定义的ViewGroup中,好处是:

1.可重用性,方便在其它的activity中使用

2.增加布局文件的可阅读性

3.如果需要改变margin,不再需要手动去调整每个view的margin

4.可以利用自定义属性去自定义ViewGroup中的每个View


在自定义ViewGroup之前,需要知道Android是如何绘制视图(views)的

    官方文档:绘制一个布局分两个过程:先测量后绘图。测量过程是通过函数measure(int, int)实现,对视图树自顶向下,依次计算,每一个view都会将自己的测量结果往下传递,最后所有的view都会记录自己的测量结果。然后就是通过layout(int, int, int,int)来绘图了,也是自顶向下绘制。

     接下来说说怎么绘制ViewGroup:

1.处理ViewGroup的width和height.

             处理width及height的操作在onMeasure()方法中进行,在此方法内,ViewGroup会根据它的子View来计算自身所占用的布局空间。

          2.布局到页面上

             这点操作在onLayout()方法中进行,在此方法中,ViewGroup会根据从onMeasure()中得到的信息将其每一个子View绘制出来。

 下面是具体的代码步骤:

 首先在XML布局文件中添加CascadeLayout:

<FrameLayout 
  <!--自定义命名空间,以便在下文中使用自定义的属性-->
  xmlns:cascade ="http://schemas.android.com/apk/res/com.manning.androidhacks.hack003"
  xmlns:android= "http://schemas.android.com/apk/res/android"
  android:layout_width= "fill_parent"
  android:layout_height= "fill_parent" >

  <com.manning.androidhacks.hack003.view.CascadeLayout
    android:layout_width ="fill_parent"
    android:layout_height ="fill_parent"
    cascade:horizontal_spacing ="30dp"   <!--因为前面添加了cascade命名空间,所以此处可以使用自定义属性-->
    cascade:vertical_spacing ="20dp" >

    <View
      android:layout_width ="100dp"
      android:layout_height ="150dp"
      cascade:layout_vertical_spacing ="90dp"<!--为子View添加的自定义属性,将在本文第三部分用到-->
      android:background ="#FF0000" />

    <View
      android:layout_width ="100dp"
      android:layout_height ="150dp"
      android:background ="#00FF00" />

    <View
      android:layout_width ="100dp"
      android:layout_height ="150dp"
      android:background ="#0000FF" />
  </com.manning.androidhacks.hack003.view.CascadeLayout>

</FrameLayout>

要使用这些自定义的属性,我们必须要定义它。

     在res/values文件夹下创建一个attrs.xml文件:

<? xml version ="1.0" encoding= "utf-8" ?>
<resources>
    <declare-styleable name= "CascadeLayout" >
        <attr name= "horizontal_spacing" format = "dimension" />
        <attr name= "vertical_spacing" format = "dimension" />
    </declare-styleable>
</resources>

然后,当我们在创建CascadeLayout且没有为其指定horizontal_spacing与vertical_spacing时,需要有一个默认值。

我们将这个默认值预先定义好并存放在res/values文件夹下的dimens.xml中:

<? xml version ="1.0" encoding= "utf-8" ?>
<resources>
    <dimen name= "cascade_horizontal_spacing" >10dp</dimen>
    <dimen name= "cascade_vertical_spacing" >10dp</dimen>
</resources>

最后,我们需要创建一个名为CascadeLayout的Java类,它继承了ViewGroup并重写了onMeasure()与OnLayout()方法。

1.CascadeLayout的构造函数

public CascadeLayout (Context context, AttributeSet attrs) {
  super( context, attrs);

  TypedArray a = context .obtainStyledAttributes (attrs ,
    R. styleable. CascadeLayout );

  try {
    mHorizontalSpacing = a. getDimensionPixelSize(
      R. styleable. CascadeLayout_horizontal_spacing ,
      getResources ().getDimensionPixelSize (
        R. dimen. cascade_horizontal_spacing ));

    mVerticalSpacing = a. getDimensionPixelSize(
      R. styleable. CascadeLayout_vertical_spacing , getResources ()
        .getDimensionPixelSize (R .dimen .cascade_vertical_spacing ));
  } finally {
    a .recycle ();
  }

2.构建自定义的LayoutParams类

     LayoutParams类将作为CascadeLayout的内部类存在,它将存储每个子View的x,y坐标。定义如下:

public static class LayoutParams extends ViewGroup .LayoutParams {
  int x;
  int y;

  public LayoutParams( Context context , AttributeSet attrs) {
    super (context , attrs );
  }

  public LayoutParams( int w , int h ) {
    super (w , h );
  }

  }

3.重写onMeasure()方法

     onMeasure()方法将是CascadeLayout类中最关键的部分,这个方法不仅计算整个ViewGroup所占用的布局空间,还将计算出每个子View所占用的布局空间。

@Override
  protected void onMeasure (int widthMeasureSpec , int heightMeasureSpec ) {
  int width = 0;
  int height = getPaddingTop ();

  final int count = getChildCount ();
  for ( int i = 0; i < count; i++) {
    View child = getChildAt (i );
    measureChild (child , widthMeasureSpec , heightMeasureSpec );
     LayoutParams lp = (LayoutParams ) child .getLayoutParams ();
    width = getPaddingLeft () + mHorizontalSpacing * i;

    lp .x = width;
    lp .y = height;

    width += child .getMeasuredWidth ();
    height += mVerticalSpacing ;
  }

  width += getPaddingRight ();
  height += getChildAt (getChildCount () - 1). getMeasuredHeight ()
    + getPaddingBottom ();

  setMeasuredDimension ( resolveSize( width, widthMeasureSpec ),
    resolveSize( height, heightMeasureSpec ));
  }

4.最后一步,重写onLayout()方法

     代码很简单,就是让每个子View都调用layout()方法。

@Override
  protected void onLayout (boolean changed, int l , int t , int r , int b ) {

  final int count = getChildCount ();
  for ( int i = 0; i < count; i++) {
    View child = getChildAt (i );
    LayoutParams lp = ( LayoutParams ) child .getLayoutParams ();

    child .layout (lp .x , lp .y , lp .x + child. getMeasuredWidth (), lp .y
      + child .getMeasuredHeight ());
  }
  }

至此,就利用自定义的ViewGroup创建了一个和图2一样效果的布局页面。

三、为子View添加自定义属性

既然费了这么大劲,怎么可能就和之前几行XML代码效果一样?

下面,我们就来为CascadeLayout中的子View添加自定义属性:

     首先,在之前创建的attrs.xml中添加如下代码:

<declare-styleable name="CascadeLayout_LayoutParams">
     <attr name="layout_vertical_spacing" format="dimension" />
</declare-styleable>

因为这个新添加的属性是以 layout_ 开头的,所以它会被添加到LayoutParams中去。

我们可以在之前自定义的内部类LayoutParams中的构造函数中读取到这个属性,将第一个构造函数改为:

public LayoutParams (Context context, AttributeSet attrs) {
    super (context , attrs );

    TypedArray a = context .obtainStyledAttributes (attrs ,
      R. styleable. CascadeLayout_LayoutParams );
    try {
    verticalSpacing = a
      .getDimensionPixelSize (
        R .styleable .CascadeLayout_LayoutParams_layout_vertical_spacing ,
        -1 );
    } finally {
    a .recycle ();
    }
  }

既然添加了新的自定义属性,就必须在onMeasure()方法中对其加以处理:

@Override
  protected void onMeasure (int widthMeasureSpec , int heightMeasureSpec ) {
    int width = getPaddingLeft ();
    int height = getPaddingTop ();
    int verticalSpacing ;

    final int count = getChildCount ();
    for ( int i = 0; i < count; i++) {
  verticalSpacing = mVerticalSpacing ;

  View child = getChildAt (i );
  measureChild (child , widthMeasureSpec , heightMeasureSpec );

  LayoutParams lp = ( LayoutParams ) child .getLayoutParams ();
  width = getPaddingLeft () + mHorizontalSpacing * i;

  lp .x = width;
  lp .y = height;

  if (lp .verticalSpacing >= 0 ) {
    verticalSpacing = lp .verticalSpacing ;
  }

  width += child .getMeasuredWidth ();
  height += verticalSpacing ;
    }

    width += getPaddingRight ();
    height += getChildAt (getChildCount () - 1). getMeasuredHeight ()
    + getPaddingBottom ();

    setMeasuredDimension ( resolveSize( width, widthMeasureSpec ),
    resolveSize( height, heightMeasureSpec ));
  }

最后附上完整的CascadeLayout代码:

package com.manning.androidhacks.hack003.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import com.manning.androidhacks.hack003.R;

public class CascadeLayout extends ViewGroup {

  private int mHorizontalSpacing;
  private int mVerticalSpacing;

  public CascadeLayout(Context context, AttributeSet attrs) {
    super(context, attrs);

    TypedArray a = context.obtainStyledAttributes(attrs,
        R.styleable.CascadeLayout);

    try {
      mHorizontalSpacing = a.getDimensionPixelSize(
          R.styleable.CascadeLayout_horizontal_spacing,
          getResources().getDimensionPixelSize(
              R.dimen.cascade_horizontal_spacing));

      mVerticalSpacing = a.getDimensionPixelSize(
          R.styleable.CascadeLayout_vertical_spacing, getResources()
              .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));
    } finally {
      a.recycle();
    }

  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = getPaddingLeft();
    int height = getPaddingTop();
    int verticalSpacing;

    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
      verticalSpacing = mVerticalSpacing;

      View child = getChildAt(i);
      measureChild(child, widthMeasureSpec, heightMeasureSpec);

      LayoutParams lp = (LayoutParams) child.getLayoutParams();
      width = getPaddingLeft() + mHorizontalSpacing * i;

      lp.x = width;
      lp.y = height;

      if (lp.verticalSpacing >= 0) {
        verticalSpacing = lp.verticalSpacing;
      }

      width += child.getMeasuredWidth();
      height += verticalSpacing;
    }

    width += getPaddingRight();
    height += getChildAt(getChildCount() - 1).getMeasuredHeight()
        + getPaddingBottom();

    setMeasuredDimension(resolveSize(width, widthMeasureSpec),
        resolveSize(height, heightMeasureSpec));
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {

    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
      View child = getChildAt(i);
      LayoutParams lp = (LayoutParams) child.getLayoutParams();

      child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
          + child.getMeasuredHeight());
    }
  }

  @Override
  protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof LayoutParams;
  }

  @Override
  protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT,
        LayoutParams.WRAP_CONTENT);
  }

  @Override
  public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
  }

  @Override
  protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return new LayoutParams(p.width, p.height);
  }

  public static class LayoutParams extends ViewGroup.LayoutParams {
    int x;
    int y;
    public int verticalSpacing;

    public LayoutParams(Context context, AttributeSet attrs) {
      super(context, attrs);

      TypedArray a = context.obtainStyledAttributes(attrs,
          R.styleable.CascadeLayout_LayoutParams);
      try {
        verticalSpacing = a
            .getDimensionPixelSize(
                R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,
                -1);
      } finally {
        a.recycle();
      }
    }

    public LayoutParams(int w, int h) {
      super(w, h);
    }

  }
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值