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

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

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

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="fill_parent"
  3. android:layout_height="fill_parent" >
  4. <View
  5. android:layout_width="100dp"
  6. android:layout_height="150dp"
  7. android:background="#FF0000" />
  8. <View
  9. android:layout_width="100dp"
  10. android:layout_height="150dp"
  11. android:layout_marginLeft="30dp"
  12. android:layout_marginTop="20dp"
  13. android:background="#00FF00" />
  14. <View
  15. android:layout_width="100dp"
  16. android:layout_height="150dp"
  17. android:layout_marginLeft="60dp"
  18. android:layout_marginTop="40dp"
  19. android:background="#0000FF" />
  20. </RelativeLayout>

效果图

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

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

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

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

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

创建CascadeLayout

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

  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. <com.manoel.view.CascadeLayout
  7. android:layout_width="fill_parent"
  8. android:layout_height="fill_parent"
  9. <!-- 自定义属性 -->
  10. cascade:horizontal_spacing="30dp"
  11. cascade:vertical_spacing="20dp" >
  12. <View
  13. android:layout_width="100dp"
  14. android:layout_height="150dp"
  15. android:background="#FF0000" />
  16. <View
  17. android:layout_width="100dp"
  18. android:layout_height="150dp"
  19. android:background="#00FF00" />
  20. <View
  21. android:layout_width="100dp"
  22. android:layout_height="150dp"
  23. android:background="#0000FF" />
  24. </com.manoel.view.CascadeLayout>
  25. </FrameLayout>

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

  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中添加如下代码:

  1. <resources>
  2. <dimen name="cascade_horizontal_spacing">10dp</dimen>
  3. <dimen name="cascade_vertical_spacing">10dp</dimen>
  4. </resources>

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

  1. public class CascadeLayout extends ViewGroup {
  2. private int mHorizontalSpacing;
  3. private int mVerticalSpacing;
  4. public CascadeLayout(Context context, AttributeSet attrs) {
  5. super(context, attrs);
  6. TypedArray a = context.obtainStyledAttributes(attrs,
  7. R.styleable.CascadeLayout);
  8. try {
  9. mHorizontalSpacing = a.getDimensionPixelSize(
  10. R.styleable.CascadeLayout_horizontal_spacing,
  11. getResources().getDimensionPixelSize(
  12. R.dimen.cascade_horizontal_spacing));
  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. @Override
  21. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  22. int width = getPaddingLeft();
  23. int height = getPaddingTop();
  24. int verticalSpacing;
  25. final int count = getChildCount();
  26. for (int i = 0; i < count; i++) {
  27. verticalSpacing = mVerticalSpacing;
  28. View child = getChildAt(i);
  29. measureChild(child, widthMeasureSpec, heightMeasureSpec);
  30. LayoutParams lp = (LayoutParams) child.getLayoutParams();
  31. width = getPaddingLeft() + mHorizontalSpacing * i;
  32. lp.x = width;
  33. lp.y = height;
  34. if (lp.verticalSpacing >= 0) {
  35. verticalSpacing = lp.verticalSpacing;
  36. }
  37. width += child.getMeasuredWidth();
  38. height += verticalSpacing;
  39. }
  40. width += getPaddingRight();
  41. height += getChildAt(getChildCount() - 1).getMeasuredHeight()
  42. + getPaddingBottom();
  43. setMeasuredDimension(resolveSize(width, widthMeasureSpec),
  44. resolveSize(height, heightMeasureSpec));
  45. }
  46. @Override
  47. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  48. final int count = getChildCount();
  49. for (int i = 0; i < count; i++) {
  50. View child = getChildAt(i);
  51. LayoutParams lp = (LayoutParams) child.getLayoutParams();
  52. child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
  53. + child.getMeasuredHeight());
  54. }
  55. }
  56. @Override
  57. protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
  58. return p instanceof LayoutParams;
  59. }
  60. @Override
  61. protected LayoutParams generateDefaultLayoutParams() {
  62. return new LayoutParams(LayoutParams.WRAP_CONTENT,
  63. LayoutParams.WRAP_CONTENT);
  64. }
  65. @Override
  66. public LayoutParams generateLayoutParams(AttributeSet attrs) {
  67. return new LayoutParams(getContext(), attrs);
  68. }
  69. @Override
  70. protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
  71. return new LayoutParams(p.width, p.height);
  72. }
  73. public static class LayoutParams extends ViewGroup.LayoutParams {
  74. int x;
  75. int y;
  76. public int verticalSpacing;
  77. public LayoutParams(Context context, AttributeSet attrs) {
  78. super(context, attrs);
  79. }
  80. public LayoutParams(int w, int h) {
  81. super(w, h);
  82. }
  83. }
  84. }

首先,分析构造函数。

  1. public CascadeLayout(Context context, AttributeSet attrs) {
  2. super(context, attrs);
  3. TypedArray a = context.obtainStyledAttributes(attrs,
  4. R.styleable.CascadeLayout);
  5. try {
  6. mHorizontalSpacing = a.getDimensionPixelSize(
  7. R.styleable.CascadeLayout_horizontal_spacing,
  8. getResources().getDimensionPixelSize(
  9. R.dimen.cascade_horizontal_spacing));
  10. mVerticalSpacing = a.getDimensionPixelSize(
  11. R.styleable.CascadeLayout_vertical_spacing, getResources()
  12. .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));
  13. } finally {
  14. a.recycle();
  15. }
  16. }

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

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

然后,分析自定义LayoutParams。

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

  1. public static class LayoutParams extends ViewGroup.LayoutParams {
  2. int x;
  3. int y;
  4. public int verticalSpacing;
  5. public LayoutParams(Context context, AttributeSet attrs) {
  6. super(context, attrs);
  7. }
  8. public LayoutParams(int w, int h) {
  9. super(w, h);
  10. }
  11. }

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

接下来,分析onMeasure()方法。

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3. int width = getPaddingLeft();
  4. int height = getPaddingTop();
  5. int verticalSpacing;
  6. final int count = getChildCount();
  7. for (int i = 0; i < count; i++) {
  8. verticalSpacing = mVerticalSpacing;
  9. View child = getChildAt(i);
  10. measureChild(child, widthMeasureSpec, heightMeasureSpec); // 令每个子视图测量自身
  11. LayoutParams lp = (LayoutParams) child.getLayoutParams();
  12. width = getPaddingLeft() + mHorizontalSpacing * i;
  13. // 保存每个子视图的x,y轴坐标
  14. lp.x = width;
  15. lp.y = height;
  16. if (lp.verticalSpacing >= 0) {
  17. verticalSpacing = lp.verticalSpacing;
  18. }
  19. width += child.getMeasuredWidth();
  20. height += verticalSpacing;
  21. }
  22. width += getPaddingRight();
  23. height += getChildAt(getChildCount() - 1).getMeasuredHeight()
  24. + getPaddingBottom();
  25. // 使用计算所得的宽和高设置整个布局的测量尺寸
  26. setMeasuredDimension(resolveSize(width, widthMeasureSpec),
  27. resolveSize(height, heightMeasureSpec));
  28. }

最后,分析onLayout()方法。

  1. @Override
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3. final int count = getChildCount();
  4. for (int i = 0; i < count; i++) {
  5. View child = getChildAt(i);
  6. LayoutParams lp = (LayoutParams) child.getLayoutParams();
  7. child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
  8. + child.getMeasuredHeight());
  9. }
  10. }

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

为子视图添加自定义属性

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

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

  1. <declare-styleable name="CascadeLayout_LayoutParams">
  2. <attr name="layout_vertical_spacing" format="dimension" />
  3. </declare-styleable>

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

  1. public static class LayoutParams extends ViewGroup.LayoutParams {
  2. int x;
  3. int y;
  4. public int verticalSpacing;
  5. public LayoutParams(Context context, AttributeSet attrs) {
  6. super(context, attrs);
  7. TypedArray a = context.obtainStyledAttributes(attrs,
  8. R.styleable.CascadeLayout_LayoutParams);
  9. try {
  10. verticalSpacing = a
  11. .getDimensionPixelSize(
  12. R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,
  13. -1);
  14. } finally {
  15. a.recycle();
  16. }
  17. }
  18. public LayoutParams(int w, int h) {
  19. super(w, h);
  20. }
  21. }

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

  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. <!-- 注意:这张“扑克牌”使用了layout_vertical_spacing属性 -->
  7. <View
  8. android:layout_width="100dp"
  9. android:layout_height="150dp"
  10. cascade:layout_vertical_spacing="90dp"
  11. android:background="#FF0000" />
  12. <View
  13. android:layout_width="100dp"
  14. android:layout_height="150dp"
  15. android:background="#00FF00" />
  16. <View
  17. android:layout_width="100dp"
  18. android:layout_height="150dp"
  19. android:background="#0000FF" />
  20. </com.manoel.view.CascadeLayout>

参考资料

如何自定义ViewGroup的更多相关文章

  1. 简单的例子了解自定义ViewGroup(一)

    在Android中,控件可以分为ViewGroup控件与View控件.自定义View控件,我之前的文章已经说过.这次我们主要说一下自定义ViewGroup控件.ViewGroup是作为父控件可以包含多 ...

  2. Android动画效果之自定义ViewGroup添加布局动画

    前言: 前面几篇文章介绍了补间动画.逐帧动画.属性动画,大部分都是针对View来实现的动画,那么该如何为了一个ViewGroup添加动画呢?今天结合自定义ViewGroup来学习一下布局动画.本文将通 ...

  3. Android自定义控件之自定义ViewGroup实现标签云

    前言: 前面几篇讲了自定义控件绘制原理Android自定义控件之基本原理(一),自定义属性Android自定义控件之自定义属性(二),自定义组合控件Android自定义控件之自定义组合控件(三),常言 ...

  4. Android自定义ViewGroup

    视图分类就两类,View和ViewGroup.ViewGroup是View的子类,ViewGroup可以包含所有的View(包括ViewGroup),View只能自我描绘,不能包含其他View. 然而 ...

  5. [Android Pro] Android开发实践:自定义ViewGroup的onLayout()分析

    reference to : http://www.linuxidc.com/Linux/2014-12/110165.htm 前一篇文章主要讲了自定义View为什么要重载onMeasure()方法( ...

  6. android 手把手教您自定义ViewGroup(一)

    1.概述 在写代码之前,我必须得问几个问题: 1.ViewGroup的职责是啥? ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属 ...

  7. 自定义ViewGroup须知

    自定义ViewGroup须知: 1.必须复写onMeasure和onLayout方法,根据容器的特性进行布局设计 2.复写onMeasure方法必须处理父布局设置宽或高为wrap_content情况下 ...

  8. Android自定义ViewGroup,实现自动换行

    学习<Android开发艺术探索>中自定义ViewGroup章节 自定义ViewGroup总结的知识点 一.自定义ViewGroup中,onMeasure理解 onMeasure(int ...

  9. 自定义ViewGroup初步探究

    由于项目需要,实现类似于地图控件,能够让一张图标自由缩放并且在其上固定位置,标记一些地点,所以在这里,我考虑了一下,决定使用自定义ViewGroup来实现.

  10. Android 自定义ViewGroup

    前面几节,我们重点讨论了自定义View的三板斧,这节我们来讨论自定义ViewGroup,为什么要自定义ViewGroup,其实就是为了更好的管理View. 自定义ViewGroup无非那么几步: Ⅰ. ...

随机推荐

  1. Linux安装配置tomcat

    1.首先配置好jdk 查看java版本:java -verson 1.官网下载jdk 2.tar -zxvf xxxx.tar.gz   解压 3.配置环境变量 <1># vi /etc/ ...

  2. WCF实现方法重载

    一.服务契约(包括回调契约)通过指定不同的OperationContract.Name来实现重载方法,当然代码部份还是必需符合C#的重载要求,即相同方法名称,不同的参数个数或参数类型 namespac ...

  3. C语言学习005:不能修改的字符串

    一段有问题的代码,你能看出来么? int main(){ char* msg="ABC"; msg[]=msg[]; puts(msg); ; } 编译这段代码并不会有什么问题,一 ...

  4. .NET Framework介绍

    .NET Framework 是一个集成在 Windows 中的组件,它支持生成和运行下一代应用程序与 XML Web Services. .NET Framework 旨在实现下列目标: 提供一个一 ...

  5. 判断JS对象是否拥有某属性

    两种方式,但稍有区别 1.in 运算符  

  6. Chrome的ERR_UNSAFE_PORT解决办法

    今天早上来上班照往常一样,打开我的VS,编译运行程序,打不开??又是一阵调试,断点,很快我发现不是我的程序问题,因为在IE,Firefox里都可以正常打开,唯独Chrome报错.又仔细看了下报错页面, ...

  7. 【Java每日一题】20161026

    20161025问题解析请点击今日问题下方的"[Java每日一题]20161026"查看 package Oct2016; import java.util.Date; publi ...

  8. sql server删除默认值(default)的方法

    不废话了----- 例如要删除student表的sex默认值 sp_help student;查询结果 找到constraiont_name的对应的值 最后 ALTER TABLE student D ...

  9. java.lang.IllegalStateException: Failed to read Class-Path attribute from manifest of jar file:/XXX

    出现这个问题的解决方案就是将原有的jar删除  然后重新下载过一遍就可以使用了  我估计是元数据等损坏了

  10. webservice入门(2)开发ws程序

    因为webservice分为服务端和客户端,所以如果要学习的话,那么肯定是包括这两部分的了. 1.开发服务端的webservice: 使用jdk开发ws其实很简单,只是需要一些注解:最重要的是 @We ...