1. 概念
Android中的View与我们以前理解的“视图”不同。在Android中,View比视图具有更广的含义,它包含了用户交互和显示,更像Windows操作系统中的window。

ViewGroup是View的子类,所以它也具有View的特性,但它主要用来充当View的容器,将其中的View视作自己的孩子,对它的子View进行管理,当然它的孩子也可以是ViewGroup类型。

ViewGroup(树根)和它的孩子们(View和ViewGroup)以树形结构形成了一个层次结构,View类有接受和处理消息的功能,android系统所产生的消息会在这些ViewGroup和 View之间传递。

2. Android的窗口系统
Android的窗口系统是Client/Server模式的,我在这里只讲窗口系统的客户端(图1)。 我们所提到的概念:View,ViewGroup,DecorView,ViewRoot都是存在于窗口系统的Client端。

Android中的Window是表示Top Level等顶级窗口的概念。DecorView是Window的Top-Level View,这个View可以称之为主View,DecorView会缺省的attach到Activity的主窗口中。

ViewRoot建立了主View(DecorView)与窗口系统Server端的通讯桥梁, ViewRoot是 Handler的子类,即它其实是个Handler,它接受窗口系统服务器端的消息并将消息投递到窗口系统的客户端(图1),然后消息就从客户端的主View往其下面的子View传递,直到消息被完全处理掉为止。

图1 窗口系统的客户端

DecorView实际上是一个ViewGroup。在依存关系上来讲,对单个主窗口来讲,DecorView是Top-Level View。View并不是关注的重点,重要的是我们需要知道消息分发路径是建立在什么关系上的。View的成员变量mParent用来管理View上级关系的。而ViewGroup顾名思义就是一组View的管理,于是在ViewGroup构建了焦点管理和子View节点数组。这样通过View的mParent和ViewGroup的mChildren构建了Android中View直接的关系网。

3. View的介绍
(1) 事件和绘制

绘制流程:

绘制按照视图树的顺序执行。视图绘制时会先绘制子控件。如果视图的背景可见,视图会在调用onDraw函数之前绘制背景。强制重绘,可以使用invalidate()。

事件的基本流程如下:
1、事件分配给相应视图,视图处理它,并通知相关监听器。
2、操作过程中如果发生视图的尺寸变化,则该视图用调用requestLayout()方法,向父控件请求再次布局。
3、操作过程中如果发生视图的外观变化,则该视图用调用invalidate()方法,请求重绘。
4、如果requestLayout()或invalidate()有一个被调用,框架会对视图树进行相关的测量、布局和绘制。
注意,视图树是单线程操作,直接调用其它视图的方法必须要在UI线程里。跨线程的操作必须使用句柄Handler。

焦点处理:
框架处理焦点的转移,来响应用户输入。isFocusable()函数表示视图是否能接受焦点。setFocusable(boolean)函数可以改变视图能否接受焦点。触摸屏模式(Touch Mode)的相关函数是isFocusableInTouchMode()和setFocusableInTouchMode(boolean)。
焦点转移按照就近算法。按哪个方向就近可以在XML布局文件中配置。
nextFocusDown
nextFocusLeft
nextFocusRight
nextFocusUp
视图请求焦点可以使用requestFocus()。

(2) 成员介绍

protected ViewParent mParent;

mParent用于记录它的父亲,就是我们前面提到的ViewGroup。

protected OnClickListener mOnClickListener;

mOnClickListener是click事件的回调接口.

大家经常使用的setOnClickListener(OnClickListener listener):

public void setOnClickListener(OnClickListener I) {

if (!isClickable()) {

setClickable(true);

}

mOnClickListener =I;

}

可以看出,mOnClickListener其实就是保存我们在应用程序中定义的OnClickListener接口的。

public void draw(Canvas canvas)

这个函数用于渲染View和它的孩子,我们不应该在子类对它进行override。

protected void onDraw(Canvas canvas)

我们一般override此函数来实现自己的绘制操作。

IWindowSession getWindowSession() {

return mAttachInfo != null ? mAttachInfo.mSession : null;

}

函数getWindowSession()用户得到窗口系统Client端和服务器端通讯的接口IWindowSession。这是一个AIDL接口,android系统中的跨进程通讯就是用AIDL接口实现的。

public final void layout(int l, int t, int r, int b)

此函数用于确定View和其子View的尺寸和位置,它的调用发生在onMeasure之后。

protected void onLayout(boolean changed, int left, int top, int right, int bottom)

此函数在layout调用完成后执行,View的子类一般override此函数,并在函数中对其每个孩子调用layout方法。

public View getRootView()

此函数用于得到View层次结构的top-level View,即上文中提到的DecorView。

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

此函数用户找出View的大小,它的参数widthMeasureSpec、heightMeasureSpec是其父亲传递给它的,这2个参数是View找出其大小时的限制条件,其实真正的精确大小确定是由onMeasure()完成的,onMeasure由measure函数调用。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

此函数测量View并根据其内容来决定View的高和宽,它应该被子类override以实现大小的精确测量。在onMeasure中我们必须调用View.setMeasuredDimension(int, int)来保存测量得到的大小,高和宽分别被保存在View.mMeasuredHeight和View.mMeasureWidth中。

public boolean onKeyUp(int keyCode, KeyEvent event)

此函数会在键盘按键释放后被调用,但前提是View必须获得焦点。

public boolean onTouchEvent(MotionEvent event)

此函数用于响应触摸屏事件。

public void invalidate()

此函数将调用onDraw,强制重绘。

public void requestLayout()

当某些东西发生改变后,当前View层次结构无效了,调用此函数对View的层次结构进行重新布局。

4. ViewGroup介绍

ViewGroup继承于View,它可以包含其他的View,就像一个View的容器,我们可以调用其成员函数addView()将View当作孩子放到ViewGroup中。

我们经常使用的LinearLayout、relativeLayout等都是ViewGroup的子类,ViewGroup类中有一个内部类ViewGroup.LayoutParams,我们经常使用LayoutParams的子类来构造布局参数。

我们也可以自定义自己的布局,以方便日后使用和维护,这时我们就需要继承ViewGroup类并在派生类中重写ViewGroup的一些方法,下面是一个简单的例子:

public class MyViewGroup extends ViewGroup {

public MyViewGroup(Context context) {

super(context);

initChilren(context); //向容器中添加孩子

}

private void initChilren (Context context) {

Button aBtn = new Button(context);

this.addView(aBtn);

Button bBtn = new Button(context);

this.addView(bBtn);

}

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b)

{

//对容器的孩子进行布局。

………………

………………

child.measure(r - l, b - t);

child.layout(0, 50, child.getMeasuredWidth(), child .getMeasuredHeight() + 50);

………………

………………

}

}


5. 不管是View还是ViewGroup,最重要一点他们的绘制原理如下:

从上图,我们可以理出大致的显示过程如下:
【1】ActivityManagerService创建Activity线程,激活一个activity
【2】系统调用Instrumentation.newActivity创建一个activity
【3】Activity创建后,attach到一个新创建的phonewindow中。这样Activity获取一个唯一的WindowManager服务的实例
【4】Activity创建过程中使用setcontentView设置用用户UI,这些VIEW被加入到PhoneWindow的ContentParent中。
【5】Activity线程继续执行,当执行到Activity.makeVisible是将根view DecoView加入到WindowManger中,WindowManger实全会为每个DecoView创建对应的ViewRoot
【6】每个ViewRoot拥有一个Surface,每个Surface将会调用底层库创建图形绘制的内存空间。这个底层库就是SurfaceFlinger。SurfaceFlinger同时也负责将个View绘制的图形合到一块(按照Z轴)显示到用户屏幕。
【7】如果用户直接在Canvas上绘制,实际上它直接操作Surface。但对每个View的变更,它是要通知到ViewRoot,然后 ViewRoot获取Canvas。如果绘制完成,surfaceFlinger得到通知,合并Surface成一个Surface到设备屏幕。
从上面的图形输出过程分析,我们可以知道真正显示图形的实际上跟Activity没有关系,完全由WindowManager来决定。 WindowManager是一个系统服务,因此可以直接调用这个服务来创建界面,并且更绝的是Dialog、Menu也是有WindowManager 来管理的。另外一个我们也可以看到,最底层都是Surface来,因此,常见开发游戏的人都推荐你使用SurfaceView来创建界面。

详细绘制流程如下:

整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简单概况为

之前状态,判断是否需要重新计算视图大小(measure)、是否重新需要安置视图的位置(layout)、以及是否需要重绘

(draw),其框架过程如下:

步骤其实为host.layout()

接下来温习一下整个View树的结构,对每个具体View对象的操作,其实就是个递归的实现。


流程一: mesarue()过程


主要作用:为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:

mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。

具体的调用链如下

ViewRoot根对象地属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调

View/ViewGroup对象的onMeasure()方法,该方法实现的功能如下:

1、设置本View视图的最终大小,该功能的实现通过调用setMeasuredDimension()方法去设置实际的高(对应属性:

mMeasuredHeight)和宽(对应属性:mMeasureWidth) ;

2 、如果该View对象是个ViewGroup类型,需要重写该onMeasure()方法,对其子视图进行遍历的measure()过程。

2.1 对每个子视图的measure()过程,是通过调用父类ViewGroup.java类里的measureChildWithMargins()方法去

现,该方法内部只是简单地调用了View对象的measure()方法。(由于measureChildWithMargins()方法只是一个过渡

更简单的做法是直接调用View对象的measure()方法)。

整个measure调用流程就是个树形的递归过程

measure函数原型为 View.java 该函数不能被重载

[java] view plain copy print ?
  1. publicfinalvoidmeasure(intwidthMeasureSpec,intheightMeasureSpec){
  2. //....
  3. //回调onMeasure()方法
  4. onMeasure(widthMeasureSpec,heightMeasureSpec);
  5. //more
  6. }

为了大家更好的理解,采用“二B程序员”的方式利用伪代码描述该measure流程

[java] view plain copy print ?
  1. //回调View视图里的onMeasure过程
  2. privatevoidonMeasure(intheight,intwidth){
  3. //设置该view的实际宽(mMeasuredWidth)高(mMeasuredHeight)
  4. //1、该方法必须在onMeasure调用,否者报异常。
  5. setMeasuredDimension(h,l);
  6. //2、如果该View是ViewGroup类型,则对它的每个子View进行measure()过程
  7. intchildCount=getChildCount();
  8. for(inti=0;i<childCount;i++){
  9. //2.1、获得每个子View对象引用
  10. Viewchild=getChildAt(i);
  11. //整个measure()过程就是个递归过程
  12. //该方法只是一个过滤器,最后会调用measure()过程;或者measureChild(child,h,i)方法都
  13. measureChildWithMargins(child,h,i);
  14. //其实,对于我们自己写的应用来说,最好的办法是去掉框架里的该方法,直接调用view.measure(),如下:
  15. //child.measure(h,l)
  16. }
  17. }
  18. //该方法具体实现在ViewGroup.java里。
  19. protectedvoidmeasureChildWithMargins(Viewv,intheight,intwidth){
  20. v.measure(h,l)
  21. }

流程二、 layout布局过程:

主要作用 :为将整个根据子视图的大小以及布局参数将View树放到合适的位置上。

具体的调用链如下:

host.layout()开始View树的布局,继而回调给View/ViewGroup类中的layout()方法。具体流程如下

1 、layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)

接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ;

2、如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。

layout函数原型为 ,位于View.java

[java] view plain copy print ?
  1. /*final标识符,不能被重载,参数为每个视图位于父视图的坐标轴
  2. *@paramlLeftposition,relativetoparent
  3. *@paramtTopposition,relativetoparent
  4. *@paramrRightposition,relativetoparent
  5. *@parambBottomposition,relativetoparent
  6. */
  7. publicfinalvoidlayout(intl,intt,intr,intb){
  8. booleanchanged=setFrame(l,t,r,b);//设置每个视图位于父视图的坐标轴
  9. if(changed||(mPrivateFlags&LAYOUT_REQUIRED)==LAYOUT_REQUIRED){
  10. if(ViewDebug.TRACE_HIERARCHY){
  11. ViewDebug.trace(this,ViewDebug.HierarchyTraceType.ON_LAYOUT);
  12. }
  13. onLayout(changed,l,t,r,b);//回调onLayout函数,设置每个子视图的布局
  14. mPrivateFlags&=~LAYOUT_REQUIRED;
  15. }
  16. mPrivateFlags&=~FORCE_LAYOUT;
  17. }


同样地, 将上面layout调用流程,用伪代码描述如下:

[java] view plain copy print ?
  1. //layout()过程ViewRoot.java
  2. //发起layout()的"发号者"在ViewRoot.java里的performTraversals()方法,mView.layout()
  3. privatevoidperformTraversals(){
  4. //...
  5. ViewmView;
  6. mView.layout(left,top,right,bottom);
  7. //....
  8. }
  9. //回调View视图里的onLayout过程,该方法只由ViewGroup类型实现
  10. privatevoidonLayout(intleft,inttop,right,bottom){
  11. //如果该View不是ViewGroup类型
  12. //调用setFrame()方法设置该控件的在父视图上的坐标轴
  13. setFrame(l,t,r,b);
  14. //--------------------------
  15. //如果该View是ViewGroup类型,则对它的每个子View进行layout()过程
  16. intchildCount=getChildCount();
  17. for(inti=0;i<childCount;i++){
  18. //2.1、获得每个子View对象引用
  19. Viewchild=getChildAt(i);
  20. //整个layout()过程就是个递归过程
  21. child.layout(l,t,r,b);
  22. }
  23. }



流程三、 draw()绘图过程

由ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不

会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该

视图需要重绘时,就会为该View添加该标志位。

调用流程 :

mView.draw()开始绘制,draw()方法实现的功能如下:

1 、绘制该View的背景

2 、为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)

3、调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)

4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)

值得说明的是,ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,应用程序一般不需要重写该方法,但可以重载父类

函数实现具体的功能。

4.1 dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个

地方“需要重绘”的视图才会调用draw()方法)。值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能

实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。

5、绘制滚动条

于是,整个调用链就这样递归下去了。

同样地,使用伪代码描述如下:

[java] view plain copy print ?
  1. //draw()过程ViewRoot.java
  2. //发起draw()的"发号者"在ViewRoot.java里的performTraversals()方法,该方法会继续调用draw()方法开始绘图
  3. privatevoiddraw(){
  4. //...
  5. ViewmView;
  6. mView.draw(canvas);
  7. //....
  8. }
  9. //回调View视图里的onLayout过程,该方法只由ViewGroup类型实现
  10. privatevoiddraw(Canvascanvas){
  11. //该方法会做如下事情
  12. //1、绘制该View的背景
  13. //2、为绘制渐变框做一些准备操作
  14. //3、调用onDraw()方法绘制视图本身
  15. //4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。
  16. //应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。
  17. //5、绘制渐变框
  18. }
  19. //ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法
  20. @Override
  21. protectedvoiddispatchDraw(Canvascanvas){
  22. //
  23. //其实现方法类似如下:
  24. intchildCount=getChildCount();
  25. for(inti=0;i<childCount;i++){
  26. Viewchild=getChildAt(i);
  27. //调用drawChild完成
  28. drawChild(child,canvas);
  29. }
  30. }
  31. //ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法
  32. protectedvoiddrawChild(Viewchild,Canvascanvas){
  33. //....
  34. //简单的回调View对象的draw()方法,递归就这么产生了。
  35. child.draw(canvas);
  36. //.........
  37. }


强调一点的就是,在这三个流程中,Google已经帮我们把draw()过程框架已经写好了,自定义的ViewGroup只需要实现

measure()过程和layout()过程即可 。


这三种情况,最终会直接或间接调用到三个函数,分别为invalidate(),requsetLaytout()以及requestFocus() ,接着

这三个函数最终会调用到ViewRoot中的schedulTraversale()方法,该函数然后发起一个异步消息,消息处理中调用

performTraverser()方法对整个View进行遍历。

invalidate()方法 :

说明:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”

视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。

一般引起invalidate()操作的函数如下:

1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。

2、setSelection()方法 :请求重新draw(),但只会绘制调用者本身。

3、setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,

继而绘制该View。

4 、setEnabled()方法 : 请求重新draw(),但不会重新绘制任何视图包括该调用者本身。

requestLayout()方法:会导致调用measure()过程 和 layout()过程 。

说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制

任何视图包括该调用者本身。

一般引起invalidate()操作的函数如下:

1、setVisibility()方法:

当View的可视状态在INVISIBLE/ VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。

同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。

requestFocus()函数说明:

说明:请求View树的draw()过程,但只绘制“需要重绘”的视图


更多相关文章

  1. 浅谈Java中Collections.sort对List排序的两种方法
  2. Python list sort方法的具体使用
  3. python list.sort()根据多个关键字排序的方法实现
  4. android上一些方法的区别和用法的注意事项
  5. android实现字体闪烁动画的方法
  6. Android(安卓)Wifi模块分析(三)
  7. Android中dispatchDraw分析
  8. Android四大基本组件介绍与生命周期
  9. Android(安卓)MediaPlayer 常用方法介绍

随机推荐

  1. PHP实现微信支付(jsapi支付)流程的方法
  2. 关于PHP中单例模式的实现
  3. PHP怎么实现微信申请退款
  4. PHP随机生成不重复的8位卡号(数字)和卡密(字
  5. php怎么获得昨天0点的时间戳
  6. PHP面试题大全(值得收藏)
  7. 关于php类的定义与实例化方法
  8. PHP实现微信模板消息发送给指定用户
  9. PHP怎么获取今天、昨天、明天的日期
  10. PHP如何使用AES加密和解密