如何自定义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>
参考资料
如何自定义ViewGroup的更多相关文章
- 简单的例子了解自定义ViewGroup(一)
在Android中,控件可以分为ViewGroup控件与View控件.自定义View控件,我之前的文章已经说过.这次我们主要说一下自定义ViewGroup控件.ViewGroup是作为父控件可以包含多 ...
- Android动画效果之自定义ViewGroup添加布局动画
前言: 前面几篇文章介绍了补间动画.逐帧动画.属性动画,大部分都是针对View来实现的动画,那么该如何为了一个ViewGroup添加动画呢?今天结合自定义ViewGroup来学习一下布局动画.本文将通 ...
- Android自定义控件之自定义ViewGroup实现标签云
前言: 前面几篇讲了自定义控件绘制原理Android自定义控件之基本原理(一),自定义属性Android自定义控件之自定义属性(二),自定义组合控件Android自定义控件之自定义组合控件(三),常言 ...
- Android自定义ViewGroup
视图分类就两类,View和ViewGroup.ViewGroup是View的子类,ViewGroup可以包含所有的View(包括ViewGroup),View只能自我描绘,不能包含其他View. 然而 ...
- [Android Pro] Android开发实践:自定义ViewGroup的onLayout()分析
reference to : http://www.linuxidc.com/Linux/2014-12/110165.htm 前一篇文章主要讲了自定义View为什么要重载onMeasure()方法( ...
- android 手把手教您自定义ViewGroup(一)
1.概述 在写代码之前,我必须得问几个问题: 1.ViewGroup的职责是啥? ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属 ...
- 自定义ViewGroup须知
自定义ViewGroup须知: 1.必须复写onMeasure和onLayout方法,根据容器的特性进行布局设计 2.复写onMeasure方法必须处理父布局设置宽或高为wrap_content情况下 ...
- Android自定义ViewGroup,实现自动换行
学习<Android开发艺术探索>中自定义ViewGroup章节 自定义ViewGroup总结的知识点 一.自定义ViewGroup中,onMeasure理解 onMeasure(int ...
- 自定义ViewGroup初步探究
由于项目需要,实现类似于地图控件,能够让一张图标自由缩放并且在其上固定位置,标记一些地点,所以在这里,我考虑了一下,决定使用自定义ViewGroup来实现.
- Android 自定义ViewGroup
前面几节,我们重点讨论了自定义View的三板斧,这节我们来讨论自定义ViewGroup,为什么要自定义ViewGroup,其实就是为了更好的管理View. 自定义ViewGroup无非那么几步: Ⅰ. ...
随机推荐
- Linux安装配置tomcat
1.首先配置好jdk 查看java版本:java -verson 1.官网下载jdk 2.tar -zxvf xxxx.tar.gz 解压 3.配置环境变量 <1># vi /etc/ ...
- WCF实现方法重载
一.服务契约(包括回调契约)通过指定不同的OperationContract.Name来实现重载方法,当然代码部份还是必需符合C#的重载要求,即相同方法名称,不同的参数个数或参数类型 namespac ...
- C语言学习005:不能修改的字符串
一段有问题的代码,你能看出来么? int main(){ char* msg="ABC"; msg[]=msg[]; puts(msg); ; } 编译这段代码并不会有什么问题,一 ...
- .NET Framework介绍
.NET Framework 是一个集成在 Windows 中的组件,它支持生成和运行下一代应用程序与 XML Web Services. .NET Framework 旨在实现下列目标: 提供一个一 ...
- 判断JS对象是否拥有某属性
两种方式,但稍有区别 1.in 运算符
- Chrome的ERR_UNSAFE_PORT解决办法
今天早上来上班照往常一样,打开我的VS,编译运行程序,打不开??又是一阵调试,断点,很快我发现不是我的程序问题,因为在IE,Firefox里都可以正常打开,唯独Chrome报错.又仔细看了下报错页面, ...
- 【Java每日一题】20161026
20161025问题解析请点击今日问题下方的"[Java每日一题]20161026"查看 package Oct2016; import java.util.Date; publi ...
- sql server删除默认值(default)的方法
不废话了----- 例如要删除student表的sex默认值 sp_help student;查询结果 找到constraiont_name的对应的值 最后 ALTER TABLE student D ...
- java.lang.IllegalStateException: Failed to read Class-Path attribute from manifest of jar file:/XXX
出现这个问题的解决方案就是将原有的jar删除 然后重新下载过一遍就可以使用了 我估计是元数据等损坏了
- webservice入门(2)开发ws程序
因为webservice分为服务端和客户端,所以如果要学习的话,那么肯定是包括这两部分的了. 1.开发服务端的webservice: 使用jdk开发ws其实很简单,只是需要一些注解:最重要的是 @We ...