Android View绘制流程

来源:互联网 发布:spark submit python 编辑:程序博客网 时间:2024/06/06 09:49

一、官方文档

     先是看了一下官方的文档, 地址是http://developer.android.com/guide/topics/ui/how-android-draws.html,它大体讲解了View的绘制流程。在此翻译一下,方便大家阅读。

     当一个Activity接收焦点,它将被要求画出它的布局。Android框架将处理画图,但Activity必须提供根节点布局的层次结构。
     从图的根节点开始布局(绘制),然后开始测量和绘制布局树。 通过遍历树和渲染来绘制每个视图,相交无效区域。反过来,每个视图组负责请求它的每个孩子绘制(draw() method)和每个视图负责绘画本身。因为树是遍历顺序,这意味着父母将被先绘制,孩子视图后被绘制。
     注意:该框架将不会画视图中所没有的无效区域,也会注意绘图视图的背景。你能强迫一个视图来画,通过使用 invalidate()方法。
     绘制视图(View)包含两个过程,一个是measure,一个是 layout。measure的过程是使用measure(int, int)方法,她也是自顶向下遍历树。在递归的过程中,每一个视图(View)将尺寸规格放入栈中,在measure过程的最后,每个视图存储了它的尺寸。layout的过程与measure差不多,先是调用layout(int, int, int, int)方法,然后自上而下遍历。在这个过程中,每个父视图(paraent)负责定位所有的孩子的位置,这个位置的数值就是由measure过程计算出来的。
     当一个视图(View)的的measure()返回,它的getMeasuredWidth() 和 getMeasuredHeight() 的值 就必须被设置了,以及所有这些视图的孩子节点。一个视图的宽度和高度值必须在父视图约束范围之内,这可以保证最后的 measure 都通过,所有的父母都接受所有孩子的测量。父视图可以调用measure()方法不止一次在它的孩子上。
     在measure过程中,使用了两个类来传递尺寸大小。一个是ViewGroup.LayoutParams,孩子视图使用这个类并告诉他们的父视图他们应该怎样被测量和放置。一个是基本的LayoutParams,用来描述视图的高度和宽度。它的尺寸可以有三种表示方法:1、具体数值 2、FILL_PARENT 3、WRAP_CONTENT
     对于不同的ViewGroup的子类,有着各自不同的LayoutParams。例如,RelativeLayout有它自己的LayoutParams的子类,其中包括子视图的能力水平和垂直中心。
  MeasureSpecs用于推动需求下树的父母和孩子。一个MeasureSpec可以在三种模式:
  UNSPECIFIED:
  EXACTLY:具体
  AT_MOST:
 
二、基本概念以及流程
     可以看到,官方的文档只是泛泛的说了一下,没有具体详细的说明整个流程,然后在网上找了一些资料,结合官方文档来详细说了下,对于理解这个部分,效果可能会更好。在正式介绍之前,还是需要理解与UI相关的基本概念。

      Activity:Activity包含一个Window,该Window在Activity的attach方法中通过调用PolicyManager.makeNewWindo创建;

     View:最基本的UI组件,表示屏幕上的一个矩形区域;

     DecorView:是Window中View的RootView,设置窗口属性;

     Window:表示顶层窗口,管理界面的显示和事件的响应;每个Activity 均会创建一个 PhoneWindow对象,是Activity和整个View系统交互的接口

     WindowManager:一个interface,继承自ViewManager。所在应用进程的窗口管理器;有一个implementation WindowManagerImpl;主要用来管理窗口的一些状态、属性、view增加、删除、更新、窗口顺序、消息收集和处理等。

     ViewRoot:通过IWindowSession接口与全局窗口管理器进行交互:界面控制和消息响应;

     ActivityThread:应用程序的主线程,其中会创建关联当前Activity与Window;创建WIndowManager实现类实例,把当前DecoView加入到WindowManager;

     Android UI的架构组成如图:

                          

    上述架构很清晰的呈现了Activity、Window、DecorView(及其组成)、ViewRoot和WMS之间的关系,通过源码简单理了下从启动Activity到创建View的过程,大致如下:

                                 

     在上图中,performLaunchActivity函数是关键函数,除了新建被调用的Activity实例外,还负责确保Activity所在的应用程序启动、读取manifest中关于此activity设置的主题信息以及上图中对“6.onCreate”调用也是通过对mInstrumentation.callActivityOnCreate来实现的。图中的“8. mContentParent.addView”其实就是架构图中phoneWindow内DecorView里面的ContentViews,该对象是一个ViewGroup类实例。在调用AddView之后,最终就会触发ViewRoot中的scheduleTraversals这个异步函数,从而进入ViewRoot的performTraversals函数,在performTraversals函数中就启动了View的绘制流程。performTraversals函数在2.3.5版本源码中就有近六百行的代码,跟我们绘制view相关的可以抽象成如下的简单流程图:

                        

    上述流程主要调用了View的measure、layout和draw三个函数。

    invalidate主要给需要重绘的视图添加DIRTY标记,并通过和父视图的矩形运算求得真正需要绘制的区域,并保存在ViewRoot中的mDirty变量中,最后调用scheduleTraversals发起重绘请求,scheduleTraversals会发送一个异步消息,最终调用performTraversals()执行重绘,。该函数可以由应用程序调用,或者由系统函数间接调用,例如setEnable(), setSelected(), setVisiblity()都会间接调用到invalidate()来请求View树重绘,更新View树的显示。注:requestLayout()和requestFocus()函数也会引起视图重绘。

    invalidate()最后会发起一个View树遍历的请求,并通过执行performTraersal()来响应该请求,performTraersal()正是对View树进行遍历和绘制的核心函数,内部的主体逻辑是判断是否需要重新测量视图大小(measure),是否需要重新布局(layout),是否重新需要绘制(draw)。measure过程是遍历的前提,只有measure后才能进行布局(layout)和绘制(draw),因为在layout的过程中需要用到measure过程中计算得到的每个View的测量大小,而draw过程需要layout确定每个view的位置才能进行绘制。下面我们主要来探讨一下measure的主要过程,相对与layout和draw,measure过程理解起来比较困难。

    我们在编写layout的xml文件时会碰到layout_width和layout_height两个属性,对于这两个属性我们有三种选择:赋值成具体的数值,match_parent或者wrap_content,而measure过程就是用来处理match_parent或者wrap_content,假如layout中规定所有View的layout_width和layout_height必须赋值成具体的数值,那么measure其实是没有必要的,但是google在设计Android的时候考虑加入match_parent或者wrap_content肯定是有原因的,它们会使得布局更加灵活。

    首先我们来看几个关键的函数和参数:

      1、public final void measue(int widthMeasureSpec, int heightMeasureSpec);

      2、protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);

      3、protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)

      4、protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)

      5、protected void measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)

   1、measure

 接着我们来看View类中measure和onMeasure函数的源码:

 

[html] view plaincopy
  1. <span style="font-family:SimSun;">public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.         if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||  
  3.                 widthMeasureSpec != mOldWidthMeasureSpec ||  
  4.                 heightMeasureSpec != mOldHeightMeasureSpec) {  
  5.   
  6.             // first clears the measured dimension flag  
  7.             mPrivateFlags &= ~MEASURED_DIMENSION_SET;  
  8.   
  9.             if (ViewDebug.TRACE_HIERARCHY) {  
  10.                 ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);  
  11.             }  
  12.   
  13.             // measure ourselves, this should set the measured dimension flag back  
  14.             onMeasure(widthMeasureSpec, heightMeasureSpec);  
  15.   
  16.             // flag not set, setMeasuredDimension() was not invoked, we raise  
  17.             // an exception to warn the developer  
  18.             if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {  
  19.                 throw new IllegalStateException("onMeasure() did not set the"  
  20.                         + " measured dimension by calling"  
  21.                         + " setMeasuredDimension()");  
  22.             }  
  23.   
  24.             mPrivateFlags |= LAYOUT_REQUIRED;  
  25.         }  
  26.   
  27.         mOldWidthMeasureSpec = widthMeasureSpec;  
  28.         mOldHeightMeasureSpec = heightMeasureSpec;  
  29.     }</span>  

  

    由于函数原型中有final字段,那么measure根本没打算被子类继承,也就是说measure的过程是固定的,而measure中调用了onMeasure函数,因此真正有变数的是onMeasure函数,onMeasure的默认实现很简单,源码如下:

[java] view plaincopy
  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
  3.                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
  4.     }  

    onMeasure默认的实现仅仅调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,而measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,一旦这两个变量被赋值,则意味着该View的测量工作结束。

[html] view plaincopy
  1. protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
  2.         mMeasuredWidth = measuredWidth;  
  3.         mMeasuredHeight = measuredHeight;  
  4.   
  5.         mPrivateFlags |= MEASURED_DIMENSION_SET;  
  6.     }  

    对于非ViewGroup的View而言,通过调用上面默认的measure——>onMeasure,即可完成View的测量,当然你也可以重载onMeasure,并调用setMeasuredDimension来设置任意大小的布局,但一般不这么做。

    对于ViewGroup的子类而言,往往会重载onMeasure函数负责其children的measure工作,重载时不要忘记调用setMeasuredDimension来设置自身的mMeasuredWidth和mMeasuredHeight。如果我们在layout的时候不需要依赖子视图的大小,那么不重载onMeasure也可以,但是必须重载onLayout来安排子视图的位置。

    再来看下measure(int widthMeasureSpec, int heightMeasureSpec)中的两个参数, 这两个参数分别是父视图提供的测量规格,当父视图调用子视图的measure函数对子视图进行测量时,会传入这两个参数,通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格,在ViewGroup的measureChildWithMargins函数中体现了这个过程。

     MeasureSpec参数的值为int型,分为高32位和低16为,高32位保存的是specMode,低16位表示specSize,specMode分三种:

      1、MeasureSpec.UNSPECIFIED,父视图不对子视图施加任何限制,子视图可以得到任意想要的大小;

      2、MeasureSpec.EXACTLY,父视图希望子视图的大小是specSize中指定的大小;

      3、MeasureSpec.AT_MOST,子视图的大小最多是specSize中的大小。

      以上施加的限制只是父视图“希望”子视图的大小按MeasureSpec中描述的那样,但是子视图的具体大小取决于多方面的。

      ViewGroup中定义了measureChildren, measureChild,  measureChildWithMargins来对子视图进行测量,measureChildren内部只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小,我们主要分析measureChildWithMargins的执行过程:

[html] view plaincopy
  1. protected void measureChildWithMargins(View child,  
  2.         int parentWidthMeasureSpec, int widthUsed,  
  3.         int parentHeightMeasureSpec, int heightUsed) {  
  4.     final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();  
  5.   
  6.     final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
  7.             mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin  
  8.                     + widthUsed, lp.width);  
  9.     final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
  10.             mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin  
  11.                     + heightUsed, lp.height);  
  12.   
  13.     child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
  14. }  

总的来看该函数就是对父视图提供的measureSpec参数进行了调整(结合自身的LayoutParams参数),然后再来调用child.measure()函数,具体通过函数getChildMeasureSpec来进行参数调整,过程如下:

[html] view plaincopy
  1. public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
  2.         int specMode = MeasureSpec.getMode(spec);  
  3.         int specSize = MeasureSpec
0 0
原创粉丝点击