android 开发技巧(3)--创建定制的 ViewGroup

在项目中,有的时候需要用到自定义的ViewGroup
一、绘制过程

绘制布局由两个遍历过程组成:测量过程和布局过程。测
量过程由 measure(int, int) 方法完成,该方法从上到下遍历视图
树。在递归遍历过程中,每个视图都会向下层传递尺寸和规格。当
measure 方法遍历结束,每个视图都保存了各自的尺寸信息。第二
个过程由 layout(int, int, int,int) 方法完成,该方法也是由上而下遍
历视图树,在遍历过程中,每个父视图通过测量过程的结果定位所
有子视图的位置信息。”
第一步是测量 ViewGroup 的宽度和高度,在 onMeasure() 方法中完成这
步操作。在该方法中, ViewGroup 通过遍历所有子视图计算出它
的大小。在 onLayout() 方法中完成,在该方法中,
ViewGroup 利用上一步计算出的测量信息,布局所有子视图。

二、ViewGroup和LayoutParams

每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等

在编写 onMeasure() 方法之前,先创建自定义 LayoutParams
类,该类用于保存每个子视图的 xy 轴位置。把 LayoutParams 定
义为 的内部类

三、View的3种测量模式

EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY;
AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST;
UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;

四、自定义ViewGroup需要实现的方法

用新定义的 ViewGroup中的LayoutParams 类,还需要重
写 CascadeLayout 类中的其他一些方法。这些方法是 checkLayoutParams()、 generateDefaultLayoutParams()、 generateLayoutPar
ams(AttributeSetattrs) 和 generateLayoutParams(ViewGroup.
LayoutParams p)。这些方法的代码在不同 ViewGroup 之间往往是
相同的。

效果图
这里写图片描述
主界面java代码就不贴了,只是引用xml而已
主界面布局activity_hack03.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:cascade="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="boerpower.com.android50hacks.Hack03Activity">

    <boerpower.com.android50hacks.CascadeLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        cascade:horizontal_spacing="30dp"
        cascade:vertical_spacing="20dp">

        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            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" />
    </boerpower.com.android50hacks.CascadeLayout>
</RelativeLayout>

自定义ViewGroup类:CascadeLayout.java

public class CascadeLayout extends ViewGroup {

  private int mHorizontalSpacing;
  private int mVerticalSpacing;

  public CascadeLayout(Context context, AttributeSet attrs) {//当通过XML文件创建该视图的实例时会调用构造函数
    super(context, attrs);

    TypedArray a = context.obtainStyledAttributes(attrs,
        R.styleable.CascadeLayout);
//mHorizontalSpacing和mVerticalSpacing由自定义属性中获取,如果其值未指定,就使用默认值
    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) {
    //使用宽和高计算布局的最终大小以及子视图的x与y轴位置
    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;

      //在 LayoutParams中保存每个子视图的 x 和 y 坐标
      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();
//在 LayoutParams中保存每个子视图的 x 和 y 坐标使用计算所得的宽和高设置整个布局的 测 量尺寸
    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);
    }

  }
}

定义那些定制的属性,在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>

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

同时还需要指定水平间距和垂直间距的默认值,以便在未指定这些值时使用。把这些默认值保存在 dimens.xml 文件中,该文件同样位于 res/values 文件夹下。 dimens.xml 文件的内容如下:

<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>

    <dimen name="cascade_horizontal_spacing">10dp</dimen>
    <dimen name="cascade_vertical_spacing">10dp</dimen>
</resources>

参考资料
Android 手把手教您自定ViewGroup
Android 自定义ViewGroup 实战篇 -> 实现FlowLayout

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值