自定义组件<六>:深入理解ViewGroup

自定义组件ViewGroup深入理解

有前几张环节可以看出来:

自定义组件的测量过程和绘制过程想弄懂并不是那么的容易。

接下里我就仔细给大家来讲一下

测量过程-:onMeasure()以及 布局过程:onLayout()

一个Viewgroup基本的继承类格式如下:

 1 import android.content.Context;
 2 import android.view.ViewGroup;
 3 
 4 public class MyViewGroup extends ViewGroup{
 5 
 6     public MyViewGroup(Context context) {
 7         super(context);
 8         // TODO Auto-generated constructor stub
 9     }
10 
11     @Override
12     protected void onLayout(boolean changed, int l, int t, int r, int b) {
13         // TODO Auto-generated method stub
14         
15     }
16 } 

如上所示,onLayout这个方法是必须要求实现的(后面具体讲解)

假设现在如下使用这个类:

 1 package com.example.myviewgroup;
 2 
 3 import android.os.Bundle;
 4 import android.widget.ImageView;
 5 import android.app.Activity;
 6 import android.graphics.Color;
 7 
 8 public class MainActivity extends Activity {
 9     MyViewGroup group;
10     ImageView imageView;
11 
12     @Override
13     public void onCreate(Bundle savedInstanceState) {
14         super.onCreate(savedInstanceState);
15         
16         group = new MyViewGroup(MainActivity.this);
17         imageView = new ImageView(this);
18         imageView.setBackgroundResource(R.drawable.ic_launcher);
19         group.addView(imageView);
20         group.setBackgroundColor(Color.GREEN);
21         setContentView(group);
22     } 
23 } 

你会发现界面上什么都没有,只是一片绿色,也就是说,子元素根本就没有被绘制上去。注意到上面有一个要求重载的方法onLayout(),重载如下:

1     @Override
2     protected void onLayout(boolean changed, int l, int t, int r, int b) {
3         // TODO Auto-generated method stub
4         for(int index = 0; index < getChildCount(); index++){
5             View v = getChildAt(index);
6             v.layout(l, t, r, b);
7         } 

这个时候图像就能显示出来了。看代码应该能基本理解原因,我们给每一个child都设定了它的现实范围,使用的方法是layout,当然这里只是显示了一个View,这里只是基本。上面传进去的四个参数分别代表着ViewGroup在整个界面上的上下左右边框,也就是说,它框定了ViewGroup的可视化范围,我们要做的就是在这个范围里面安排我们的子View。再继续,假设我们这样使用自定义的ViewGroup:

 1 package com.example.myviewgroup;
 2 
 3 import android.os.Bundle;
 4 import android.widget.ImageView;
 5 import android.widget.LinearLayout;
 6 import android.widget.TextView;
 7 import android.app.Activity;
 8 import android.graphics.Color;
 9 
10 public class MainActivity extends Activity {
11     LinearLayout layout;
12     
13     MyViewGroup group;
14     TextView textView;
15     ImageView imageView;
16     @Override
17     public void onCreate(Bundle savedInstanceState) {
18         super.onCreate(savedInstanceState);
19         
20         layout = new LinearLayout(this);
21         group = new MyViewGroup(this);
22         imageView = new ImageView(this);
23         textView = new TextView(this);
24         
25         imageView.setBackgroundResource(R.drawable.ic_launcher);
26         textView.setText("Hello");
27         
28         layout.setOrientation(LinearLayout.VERTICAL);
29         layout.setBackgroundColor(Color.WHITE);
30         
31         layout.addView(imageView);
32         layout.addView(textView);
33         group.addView(layout, new LinearLayout.LayoutParams(100, 100));
34         group.setBackgroundColor(Color.GREEN);
35         setContentView(group);
36     } 
37 } 

我们会发现,整个界面又和以前一样,只显示一片绿色了,组件又不见了,你可以尝试改变layout的背景颜色,会发现最后显示的界面颜色也变化了,所以可以判定,我们这样子写,只是显示了最最外层的代码,并没有触发整个布局去绘制她自己的子View(这里指的是imageView和textView)。前面说到onLayout方法提供整个组件的可视范围以便于子View布局,那么子View的大小如何确定以及当子View是一个ViewGroup的时候怎么触发它去绘制自己的子View呢?这涉及ViewGroup的另外一个方法:

1 @Override
2     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3         // TODO Auto-generated method stub
4         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
5     } 

这个方法来自View,而不是ViewGroup的,文档解释如下:

Measure the view and its content to determine the measured width and the measured height. This method is invoked by measure(int, int) and should be overriden by subclasses to provide accurate and efficient measurement of their contents.

通俗解释一下:这个方法其实是用来丈量View本身以及它自己的尺寸的!什么意思呢?我们先看看传入的参数是什么。传入的参数是两个int,但是实际上这两个int大有文章,是两个int的&值,解释如下:

两个参数分别代表宽度和高度的MeasureSpec,android2.2文档中对于MeasureSpec中的说明是: 一个MeasureSpec封装了从父容器传递给子容器的布局需求.每一个MeasureSpec代表了一个宽度,或者高度的说明.一个MeasureSpec是一个大小跟模式的组合值.一共有三种模式.

(1)UPSPECIFIED:父容器对于子容器没有任何限制,子容器想要多大就多大.

(2) EXACTLY:父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间.

(3) AT_MOST:子容器可以是声明大小内的任意大小.

暂时先这样解释着,后面再去细说。总之,这两个参数传进来的是本View(ViewGroup)显示的长和宽的值和某个模式的&值,具体取出模式或者值的方法如下:

1         int widthMode = MeasureSpec.getMode(widthMeasureSpec); 
2         int heightMode = MeasureSpec.getMode(heightMeasureSpec); 
3            
4         int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
5         int heightSize = MeasureSpec.getSize(heightMeasureSpec);  

而合成则可以使用下面的方法:

1 MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST)

OK,上面是一些介绍,到这里可能比较混乱,整理一下:

如果让你在一个界面上绘制一个矩形,为了准确的画出这个矩形,你必须知道两件事情:1)矩形的位置(暂定为左上角的坐标);2)尺寸(长和宽),Android绘制图形的时候也要知道这两件事情,前面已经介绍了几个方法了,现在把它们联系起来(你可以想象,你用一个layoutA作为contentView,然后在layoutA里面要加一个button),Android会怎么去做呢?最正规的解释当然源自Android官方文档:http://developer.android.com/guide/topics/ui/how-android-draws.html

首先看一下View树的样子:

我们的界面基本上就是以这样子的方式组织展现的。

When an Activity receives focus, it will be requested to draw its layout. The Android framework will handle the procedure for drawing, but the Activity must provide the root node of its layout hierarchy.

(当一个Activity获取焦点的时候,它就会被要求去画出它的布局。Android框架会处理绘画过程,但是Activity必须提供布局的根节点,在上面的图上,我们可以理解为最上面的ViewGroup,而实际上还有一个更深的root)

Drawing begins with the root node of the layout. It is requested to measure and draw the layout tree. Drawing is handled by walking the tree and rendering each View that intersects the invalid region. In turn, each View group is responsible for requesting each
of its children to be drawn (with the draw() method)
and each View is responsible for drawing itself. Because the tree is traversed in-order, this means that parents will be drawn before (i.e., behind) their children, with siblings drawn in the order they appear in the tree.

(绘画开始于布局的根节点,要求测量并且画出整个布局树。绘画通过遍历整个树来完成,不可见的区域的View被放弃。每个ViewGroup负责要求它的子View去绘画,每个子View则负责去绘画自己。因为布局树是顺序遍历的,这意味着父View在子View之前被画出来(这个符合常理,后面解释))。

注解:假设一个TextView设置为(FILL_PAREMT, FILL_PARENT),则很明显必须先画出父View的尺寸,才能去画出这个TextView,而且从上至下也就是先画父View再画子View,显示的时候才正常,否则父View会挡住子View的显示。

Drawing the layout is a two pass process: a measure pass and a layout pass. The measuring pass is implemented in measure(int,
int)
 and is a top-down traversal of the View tree. Each View pushes dimension specifications down the tree during the recursion. At the end of the measure pass, every View has stored its measurements. The second pass happens in layout(int,
int, int, int)
 and is also top-down. During this pass each parent is responsible for positioning all of its children using the sizes computed in the measure pass.

(布局绘画涉及两个过程:测量过程和布局过程。测量过程通过measure方法实现,是View树自顶向下的遍历,每个View在循环过程中将尺寸细节往下传递,当测量过程完成之后,所有的View都存储了自己的尺寸。第二个过程则是通过方法layout来实现的,也是自顶向下的。在这个过程中,每个父View负责通过计算好的尺寸放置它的子View。)

注解:这和前面说的一样,一个过程是用来丈量尺寸的,一个过程是用来摆放位置的。

When a View‘s measure() method returns, its getMeasuredWidth() and getMeasuredHeight() values
must be set, along with those for all of that View‘s descendants. A View‘s measured width and measured height values must respect the constraints imposed by the View‘s parents. This guarantees that at the end of the measure pass, all parents accept all of
their children‘s measurements. A parent View may call measure() more than once on its children. For example, the parent may measure each child once with unspecified dimensions to find out how big they want to be, then call measure() on them again with actual
numbers if the sum of all the children‘s unconstrained sizes is too big or too small (i.e., if the children don‘t agree among themselves as to how much space they each get, the parent will intervene and set the rules on the second pass).

(当一个View的measure()方法返回的时候,它的getMeasuredWidth和getMeasuredHeight方法的值一定是被设置好的。它所有的子节点同样被设置好。一个View的测量宽和测量高一定要遵循父View的约束,这保证了在测量过程结束的时候,所有的父View可以接受子View的测量值。一个父View或许会多次调用子View的measure()方法。举个例子,父View会使用不明确的尺寸去丈量看看子View到底需要多大,当子View总的尺寸太大或者太小的时候会再次使用实际的尺寸去调用onmeasure().)

The measure pass uses two classes to communicate dimensions. The ViewGroup.LayoutParams class
is used by Views to tell their parents how they want to be measured and positioned. The base LayoutParams class just describes how big the View wants to be for both width and height. For each dimension, it can specify one of:

  • an exact number
  • FILL_PARENT, which means the View wants to be as big as its parent (minus padding)
  • WRAP_CONTENT, which means that the View wants to be just big enough to enclose its content (plus padding).

不解释。

There are subclasses of LayoutParams for different subclasses of ViewGroup. For example, RelativeLayout has its own subclass of LayoutParams, which includes the ability to center child Views horizontally and vertically.

MeasureSpecs are used to push requirements down the tree from parent to child. A MeasureSpec can be in one of three modes:

  • UNSPECIFIED: This is used by a parent to determine the desired dimension of a child View. For example, a LinearLayout may call measure() on its child with the height
    set to UNSPECIFIED and a width of EXACTLY240 to find out how tall the child View wants to be given a width of 240 pixels.
  • EXACTLY: This is used by the parent to impose an exact size on the child. The child must use this size, and guarantee that all of its descendants will fit within this size.
  • AT_MOST: This is used by the parent to impose a maximum size on the child. The child must guarantee that it and all of its descendants will fit within this size.

这里前面已经提到过,也不多说,注意红色部分,也就是说可以通过设置高为一个确定值(通过EXACTLY)来看看子View在这个宽度下会怎么确定自己的高度。

OKOK,再休息一下。上面的问题可以得到解决了,往重载的ViewGroup里面添加Layout子View的时候,我们需要重载如下:

1     @Override
2     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3             caculateWidthAndPadding(MeasureSpec.getSize(widthMeasureSpec));
4         for(int index = 0; index < getChildCount(); index++){
5 
6                 child.measure(MeasureSpec.makeMeasureSpec(childSize, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(childSize, MeasureSpec.AT_MOST));
7         }
8         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
9     }

当然,具体的measure里面传入的参数你可以自己决定,我在这里根据widthMeasureSpec计算出一个子View的宽度(childSize),然后告诉所有的childView,你使用的最大尺寸就是childSize,不能超过(通过childSize),这个方法则会触发子View的onMeasure()方法,去设置子View的布局,由此我们可以可以看到onMeasure这个方法的作用:

1)在这个方法里面会循环调用子View的measure方法,不停的往下触发子View去丈量自己的尺寸;

2)ViewGroup继承于View,onMeasure方法在View类中的源码如下:

1     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
2         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
3                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
4     } 

关于getDefaultSize方法就不多说了,看看setMeasureDimension,源码如下:

 1     /**
 2      * <p>This mehod must be called by {@link #onMeasure(int, int)} to store the
 3      * measured width and measured height. Failing to do so will trigger an
 4      * exception at measurement time.</p>
 5      *
 6      * @param measuredWidth the measured width of this view
 7      * @param measuredHeight the measured height of this view
 8      */
 9     protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
10         mMeasuredWidth = measuredWidth;
11         mMeasuredHeight = measuredHeight;
12 
13         mPrivateFlags |= MEASURED_DIMENSION_SET;
14     } 

看到木有,它设置了自己的宽和高!我们在重载View的时候,如果重载了onMeasure方法,就一定要调用setMeasureDimension方法,否则会抛出异常,而重载View‘Group的时候,则只需要调用super.OnMeasure即可。

最后整理一下:

1)测量过程------>onMeasure(),传入的参数是本View的可见长和宽,通过这个方法循环测量所有View的尺寸并且存储在View里面;

2)布局过程------>onLayout(),传入的参数是View可见区域的上下左右四边的位置,在这个方法里面可以通过layout来放置子View;

补充:getWidth()和getMeasuredWidth()的区别

getWidth(): View在设定好布局后,整个View的宽度

getMeasuredWidth():对View上的内容进行测量后得到的View内容占据的宽度。

很简单,getWidth()就是View显示之后的width,而getMeasuredWidth,从前面的源代码就可以看出来其实是在measure里面传入的参数,具体是否一样完全要看程序最后的计算。

时间: 2024-10-09 17:56:55

自定义组件<六>:深入理解ViewGroup的相关文章

1、开发自定义组件简要

一.自定义组件分类 Customized Component:继承VIew ,增加更多的属性和事件.  横向扩展 Compound Component: 继承ViewGroup , 把多个简单控件通过布局拼装一个复合控件.横向扩展 二.定义组件步骤 1 选择继承类(肯定是View or View的子类). 2 类的初始化:新增属性    属性的初值设定. 3 重载方法: 布局及呈现onDraw() , onMeasure() 事件(自定义事件)onKeyEvent() ...  监听器/重载函数

Flex自定义组件开发之日周月日期选择日历控件

原文:Flex自定义组件开发之日周月日期选择日历控件 使用过DateField的我们都知道,DateField 控件是用于显示日期的文本字段,字段右侧带有日历图标.当用户在控件边框内的任一位置单击时,将弹出一个 DateChooser 控件,显示当月的所有日期.如果未选择日期,则该文本字段为空白,并且 DateChooser 控件中将显示当前日期的月份.当 DateChooser 控件处于打开状态时,用户可以在各个月份和年份之间滚动,并选择某个日期.选择日期后,DateChooser 控件关闭,

Android自定义组件系列【6】——进阶实践(3)

上一篇<Android自定义组件系列[5]--进阶实践(2)>继续对任老师的<可下拉的PinnedHeaderExpandableListView的实现>进行了分析,这一篇计划中间插一段"知识点",对Android中的事件分发机制进行解析.细心的朋友可能会发现,打开大牛写的Android项目,里面很多组件都是自定义的(这就是为什么界面和体验这么吸引你的原因),但是要灵活的去自定义组件就必须对手势(也就是各种监听)必须熟悉,能处理好事件之间的关系. 先看一段代码:

Android自定义组件系列【10】——随ViewPager滑动的导航条

昨天在用到ViewPager实现滑动导航的时候发现微信的导航条效果是跟随ViewPager的滑动而动的,刚开始想了一下,感觉可以使用动画实现,但是这个滑动是随手指时时变化的,貌似不可行,后来再网上搜了一下,找到一个开源代码,结果打开一看大吃一惊,这么简单的效果代码居然大概有300多行,太占手机存储空间了!后来自己干脆重写ViewGroup使用scrollTo方法实现了一下,具体实现过程如下: package com.example.slideupdownviewpage; import andr

Android自定义组件系列【5】——进阶实践(1)

简介 项目开发中发现问题.解决问题这个过程中会出现很多问题,比如重复出现.某个问题的遗留,这些问题的本质就是设计模式.今天记录设计模式的知识点. 内容 在java以及其他的面向对象设计模式中,类与类之间主要有6种关系,他们分别是:依赖.关联.聚合.组合.继承.实现.它们的耦合度依次增强. 依赖关系:对于两个相对独立的对象,当一个对象负责构造另一个对象的实例,或者依赖另一个对象的服务时,这两个对象之间主要体现为依赖关系.关联关系:分为单向关联和双向关联.在java中,单向关联表现为:类A当中使用了

Qsys自定义组件的开始-Avalon总线规范(中文)

学习FPGA这么长时间了,一直没有整理自己的学习内容,这回要把每一段时间的学习内容总结一下,就从自定义组件开始吧.一定要坚持下来呀!! Avalon 总线规范 参考手册   (Avalon从端口传输与流模式从端口传输部分)  //*************************************    http://www.altera.com 免责声明: 本手册原自Altera 公司发布的<Avalon Bus Specification-Reference Manual>,一切权力

Android开发——构建自定义组件

Android中,你的应用程序程序与View类组件有着一种固定的联系,例如按钮(Button). 文本框(TextView), 可编辑文本框(EditText), 列表框(ListView), 复选框(CheckBox), 单选框(RadioButton), 滚动条(Gallery), 微调器(Spinner), 等等,还有一些比较先进的有着特殊用途的View组件,例如 AutoCompleteTextView, ImageSwitcher和 TextSwitcher.除此之外,种类繁多的像 线

Vue.extend提供自定义组件的构造器

Vue.extend 返回的是一个“扩展实例构造器”,也就是预设了部分选项的Vue实例构造器.经常服务于Vue.component用来生成组件,可以简单理解为当在模板中遇到该组件名称作为标签的自定义元素时,会自动调用“扩展实例构造器”来生产组件实例,并挂载到自定义元素上. 自定义无参数标签 我们想象一个需求,需求是这样的,要在博客页面多处显示作者的网名,并在网名上直接有链接地址.我们希望在html中只需要写<message></message> ,这和自定义组件很像,但是他没有传递

【Android 应用开发】 自定义组件 宽高适配方法, 手势监听器操作组件, 回调接口维护策略, 绘制方法分析 -- 基于 WheelView 组件分析自定义组件

博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569 代码下载 : -- GitHub : https://github.com/han1202012/WheelViewDemo.git -- CSDN : http://download.csdn.net/detail/han1202012/8208997 ; 博客总结 : 博文内容 : 本文完整地分析了 WheelView 所有的源码, 包括其适配器类型, 两种回调接