【Android 开发技巧】逼格提升指南 —— 如何自定义ViewGroup

本文由 孙国威 原创。如需转载,请注明出处!

这篇文章准备说说如何自定义ViewGroup。对于新手来说,自定义ViewGroup是那种大牛级别的人物才能掌握的东西,自己却望而生畏。

不要怕,请谨记“一切新事物都是纸老虎,只要肯去花时间钻研,没有学不会的东西”。

废话不多说,接下来,就让我们来揭开自定义ViewGroup的神秘面纱。

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

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

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

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

效果图

没错,通过这种方式是可以实现的。但是,不觉得这种方式有点low吗?!让我们用高级一点的方式去实现它,提升一下自己的逼格!

定制ViewGroup之前,我们需要先理解几个概念。

Android绘制视图的方式
这里我不会涉及太多的细节,但是需要理解Android开发文档中的一段话:

“绘制布局由两个遍历过程组成:测量过程和布局过程。测量过程由measure(int, int)方法完成,该方法从上到下遍历视图树。在递归遍历过程中,每个视图都会向下层传递尺寸和规格。当measure方法遍历结束,每个视图都保存了各自的尺寸信息。第二个过程由layout(int,int,int,int)方法完成,该方法也是由上而下遍历视图树,在遍历过程中,每个父视图通过测量过程的结果定位所有子视图的位置信息。”

简而言之,第一步是测量ViewGroup的宽度和高度,在onMeasure()方法中完成,ViewGroup遍历所有子视图计算出它们的大小。第二步是根据第一步获取的尺寸去布局所有子视图,在onLayout()中完成。

创建CascadeLayout

终于到了定制ViewGroup的阶段了。假设我们已经定制了一个CascadeLayout的容器,我们会这样使用它。

<FrameLayout xmlns:cascade="http://schemas.android.com/apk/res/com.manoel.custom"
    <!-- 声明命名空间 -->
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <com.manoel.view.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" />
    </com.manoel.view.CascadeLayout>

</FrameLayout>

首先,定义属性。在values文件夹下面创建attrs.xml,代码如下:

<resources>
    <declare-styleable name="CascadeLayout">
        <attr name="horizontal_spacing" format="dimension" />
        <attr name="vertical_spacing" format="dimension" />
    </declare-styleable>
</resources>

同时,为了严谨一些,定义一些默认的垂直距离和水平距离,以防在布局中没有提供这些属性。

在dimens.xml中添加如下代码:

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

准备工作已经做好了,接下来看一下CascadeLayout的源码,略微有点长,后面帮助大家分析一下。

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);
    }

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

  }
}

首先,分析构造函数。

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();
    }

  }

如果在布局中使用CasecadeLayout,系统就会调用这个构造函数,这个大家都应该知道的吧。这里不解释why,有兴趣的可以去看源码,重点看系统是如何解析xml布局的。

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

然后,分析自定义LayoutParams。

这个类的用途就是保存每个子视图的x,y轴位置。这里把它定义为静态内部类。ps:提到静态内部类,我又想起来关于多线程内存泄露的问题了,如果有时间再给大家解释一下多线程造成内存泄露的问题。

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

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

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

  }

除此之外,还需要重写一些方法,checkLayoutParams()、generateDefaultLayoutParams()等,这个方法在不同ViewGroup之间往往是相同的。

接下来,分析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;
      // 保存每个子视图的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();
    // 使用计算所得的宽和高设置整个布局的测量尺寸
    setMeasuredDimension(resolveSize(width, widthMeasureSpec),
        resolveSize(height, heightMeasureSpec));
  }

最后,分析onLayout()方法。

  @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());
    }
  }

逻辑很简单,用onMeasure()方法计算出的值为参数循环调用子View的layout()方法。

为子视图添加自定义属性

作为示例,下面将添加子视图重写垂直间距的方法。

第一步是向attrs.xml中添加一个新的属性。

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

这里的属性名是layout_vertical_spacing,因为该属性名前缀是layout_,同时,又不是View固有的属性,所以该属性会被添加到LayoutParams的属性表中。在CascadeLayout类的构造函数中读取这个新属性。

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);
    }

  }

那怎么使用这个属性呢?so easy!

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

        <!-- 注意:这张“扑克牌”使用了layout_vertical_spacing属性 -->
        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            cascade:layout_vertical_spacing="90dp"
            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.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

后记

呼~这就是这篇文章所有的内容了。大家应该对如何自定义ViewGroup有了一个感性的认识了。

想要精通自定义ViewGroup,办法只有一个,那就是多动脑,勤动手!

好了,要说的就这么多,如果有什么问题,尽管留言,大家一起交流,一起进步!一起提升逼格,一起做UI大神!

时间: 2024-08-06 03:04:50

【Android 开发技巧】逼格提升指南 —— 如何自定义ViewGroup的相关文章

Android开发技巧——自定义控件之自定义属性

Android开发技巧--自定义控件之自定义属性 掌握自定义控件是很重要的,因为通过自定义控件,能够:解决UI问题,优化布局性能,简化布局代码. 上一篇讲了如何通过xml把几个控件组织起来,并继承某个ViewGroup子类,把它们封装起来使用.这是我们接触到的最简单的一种自定制控件了.但许多时候,我们还需要在布局文件中使用它们的时候,能通过属性传入一些值,来影响最终的显示结果. 我们在做项目中经常会遇到的一个情况:一张图片加一个文本的组合.比如充值账户成功之后显示的一个界面,上面是一个表示成功的

Android开发技巧——实现可复用的ActionSheet菜单

在上一篇<Android开发技巧--使用Dialog实现仿QQ的ActionSheet菜单>中,讲了这种菜单的实现过程,接下来将把它改成一个可复用的控件库. 本文原创,转载请注明出处: http://blog.csdn.net/maosidiaoxian/article/details/46324941 对于要实现的可复用的控件库,我需要它具备以下两点: 可添加远程依赖(不考虑Eclipse中的使用) 可灵活配置 分离库的实现代码 对于第一点,需要做的就是在Android Studio中新建一

Android开发技巧——使用PopupWindow实现弹出菜单

在本文当中,我将会与大家分享一个封装了PopupWindow实现弹出菜单的类,并说明它的实现与使用. 因对界面的需求,android原生的弹出菜单已不能满足我们的需求,自定义菜单成了我们的唯一选择,在本文当中,我将与大家分享如何使用PopupWindow实现弹出菜单. 1.弹出菜单的封装PopMenu PopupWindow可以说是一个浮动在Activity之上的容器,通常用来显示自定义的视图.比如像自动完成输入框AutoCompleteTextView,它的提示列表就是使用PopupWindo

android开发最常用例子整理----(1)自定义按钮实现

android开发最常用例子整理----(1)自定义按钮实现 一.Activity MainActivity.java源码: public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }

Android 开发技巧总结(三)

1.在界面中按返回键判断某个控件的可见性,如果可见,则用动画让其消失,应用并不退出. Onkeydown方法中: if(keyCode == 4){ if(slidingMenu.getVisibility() == View.VISIBLE){ //退出的动画 AnimationSet animationSet = new AnimationSet(true); TranslateAnimation animation = new TranslateAnimation(0,-280, 0, 0

50个Android开发技巧(10 为TextView加入样式)

首先来看一个控件的例子: (原文地址:http://blog.csdn.net/vector_yi/article/details/24428085) 手机上类似这种场景你一定已经见过非常多次了,但有没有考虑过它是如何实现的? 或许你会觉得这是一个略微复杂的自己定义的View,但,没错,这实际上仅仅是一个原生的TextView而已. TextView是一个简单而奇妙的Widget,你能够利用它以不同风格不同格式来展现文字. 举两个简单的样例: 1.在TextView中加入一个超链接 事实上非常e

android开发最常用例子整理----(2)自定义ListView(SimpleAdapter实现)

android开发最常用例子整理----(2)自定义ListView(SimpleAdapter实现) 一.Activity MainActivity.java源码: public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layou

50个Android开发技巧(24 处理ListView数据为空的情况)

在移动平台上为用户展示数据的一个经常用法是将数据填充进一个List内,而此时须要注意的一点就是: 原文地址:(http://blog.csdn.net/vector_yi/article/details/24936163) 怎样处理须要填充的数据为空的情况? ListView及其它继承自AdapterView的类都有一个简便的处理这样的情况的方法:setEmptyView(View). 当ListView的Adapter为空或者Adapter的isEmpty()方法返回true的时候,它将会把设

Android开发技巧——大图裁剪

本篇内容是接上篇<Android开发技巧--定制仿微信图片裁剪控件> 的,先简单介绍对上篇所封装的裁剪控件的使用,再详细说明如何使用它进行大图裁剪,包括对旋转图片的裁剪. 裁剪控件的简单使用 XML代码 使用如普通控件一样,首先在布局文件里包含该控件: <com.githang.clipimage.ClipImageView xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+i