Android中View绘制流程以及invalidate()等相关方法分析 .

来源:互联网 发布:淘宝冰点价怎么设置 编辑:程序博客网 时间:2024/06/16 14:01
转载请注明出处:http://blog.csdn.net/qinjuning

 

 

 

            前言: 本文是我读《Android内核剖析》第13章----View工作原理总结而成的,在此膜拜下作者 。同时真挚地向渴望了解

     Android 框架层网友,推荐这本书,希望你们能够在Android开发里学到更多的知识 。 

 

         

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

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

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

                                                                                                   步骤其实为host.layout() 

           

 

 

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

 

                  

 

           关于这个 DecorView 根视图的说明,可以参考我的这篇博客:

               

         《Android中将布局文件/View添加至窗口过程分析 ---- 从setContentView()谈起》



  流程一:      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 plaincopyprint?
  1. public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     //....   
  3.   
  4.     //回调onMeasure()方法    
  5.     onMeasure(widthMeasureSpec, heightMeasureSpec);  
  6.      
  7.     //more  
  8. }  

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

 

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

流程二、 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 plaincopyprint?
  1. /* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴 
  2.  * @param l Left position, relative to parent 
  3.  * @param t Top position, relative to parent 
  4.  * @param r Right position, relative to parent 
  5.  * @param b Bottom position, relative to parent 
  6.  */  
  7. public final void layout(int l, int t, int r, int b) {  
  8.     boolean changed = 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.   
  14.         onLayout(changed, l, t, r, b);//回调onLayout函数 ,设置每个子视图的布局  
  15.         mPrivateFlags &= ~LAYOUT_REQUIRED;  
  16.     }  
  17.     mPrivateFlags &= ~FORCE_LAYOUT;  
  18. }  


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

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



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


   关于绘制背景图片详细的过程,请参考我的另外的博客:

           

              <<Android中View(视图)绘制不同状态背景图片原理深入分析以及StateListDrawable使用详解>>


    强调一点的就是,在这三个流程中,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()过程,但只绘制“需要重绘”的视图。

 

 

    下面写个简单的小Demo吧,主要目的是给大家演示绘图的过程以及每个流程里该做的一些功能。截图如下:


                                                



 1、    MyViewGroup.java  自定义ViewGroup类型

   

[java] view plaincopyprint?
  1. /** 
  2.  * @author http://http://blog.csdn.net/qinjuning 
  3.  */  
  4. //自定义ViewGroup 对象  
  5. public class MyViewGroup  extends ViewGroup{  
  6.   
  7.   
  8.     private static String TAG = "MyViewGroup" ;  
  9.     private Context mContext ;  
  10.       
  11.     public MyViewGroup(Context context) {  
  12.         super(context);  
  13.         mContext = context ;  
  14.         init() ;  
  15.     }  
  16.   
  17.     //xml定义的属性,需要该构造函数  
  18.     public MyViewGroup(Context context , AttributeSet attrs){  
  19.         super(context,attrs) ;  
  20.         mContext = context ;  
  21.         init() ;  
  22.     }  
  23.       
  24.     //为MyViewGroup添加三个子View  
  25.     private void init(){  
  26.         //调用ViewGroup父类addView()方法添加子View  
  27.           
  28.         //child 对象一 : Button  
  29.         Button btn= new Button(mContext) ;  
  30.         btn.setText("I am Button") ;  
  31.         this.addView(btn) ;  
  32.           
  33.         //child 对象二 : ImageView   
  34.         ImageView img = new ImageView(mContext) ;  
  35.         img.setBackgroundResource(R.drawable.icon) ;  
  36.         this.addView(img) ;  
  37.           
  38.         //child 对象三 : TextView  
  39.         TextView txt = new TextView(mContext) ;  
  40.         txt.setText("Only Text") ;  
  41.         this.addView(txt) ;   
  42.           
  43.         //child 对象四 : 自定义View  
  44.         MyView myView = new MyView(mContext) ;  
  45.         this.addView(myView) ;   
  46.     }  
  47.       
  48.     @Override  
  49.     //对每个子View进行measure():设置每子View的大小,即实际宽和高  
  50.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){  
  51.         //通过init()方法,我们为该ViewGroup对象添加了三个视图 , Button、 ImageView、TextView  
  52.         int childCount = getChildCount() ;  
  53.         Log.i(TAG, "the size of this ViewGroup is ----> " + childCount) ;  
  54.                           
  55.         Log.i(TAG, "**** onMeasure start *****") ;  
  56.           
  57.         //获取该ViewGroup的实际长和宽  涉及到MeasureSpec类的使用  
  58.         int specSize_Widht = MeasureSpec.getSize(widthMeasureSpec) ;  
  59.         int specSize_Heigth = MeasureSpec.getSize(heightMeasureSpec) ;  
  60.           
  61.         Log.i(TAG, "**** specSize_Widht " + specSize_Widht+ " * specSize_Heigth   *****" + specSize_Heigth) ;  
  62.           
  63.         //设置本ViewGroup的宽高  
  64.         setMeasuredDimension(specSize_Widht , specSize_Heigth) ;  
  65.           
  66.           
  67.           
  68.           
  69.         for(int i=0 ;i<childCount ; i++){  
  70.             View child = getChildAt(i) ;   //获得每个对象的引用  
  71.             child.measure(5050) ;   //简单的设置每个子View对象的宽高为 50px , 50px    
  72.             //或者可以调用ViewGroup父类方法measureChild()或者measureChildWithMargins()方法  
  73.             //this.measureChild(child, widthMeasureSpec, heightMeasureSpec) ;  
  74.         }  
  75.           
  76.     }  
  77.       
  78.     @Override  
  79.     //对每个子View视图进行布局  
  80.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  81.         // TODO Auto-generated method stub  
  82.         //通过init()方法,我们为该ViewGroup对象添加了三个视图 , Button、 ImageView、TextView  
  83.         int childCount = getChildCount() ;  
  84.           
  85.         int startLeft = 0 ;//设置每个子View的起始横坐标   
  86.         int startTop = 10 ; //每个子View距离父视图的位置 , 简单设置为10px吧 。 可以理解为 android:margin=10px ;  
  87.           
  88.         Log.i(TAG, "**** onLayout start ****") ;  
  89.         for(int i=0 ;i<childCount ; i++){  
  90.             View child = getChildAt(i) ;   //获得每个对象的引用  
  91.             child.layout(startLeft, startTop, startLeft+child.getMeasuredWidth(), startTop+child.getMeasuredHeight()) ;  
  92.             startLeft =startLeft+child.getMeasuredWidth() + 10;  //校准startLeft值,View之间的间距设为10px ;  
  93.             Log.i(TAG, "**** onLayout startLeft ****" +startLeft) ;  
  94.         }             
  95.     }  
  96.     //绘图过程Android已经为我们封装好了 ,这儿只为了观察方法调用程  
  97.     protected void dispatchDraw(Canvas canvas){  
  98.         Log.i(TAG, "**** dispatchDraw start ****") ;  
  99.           
  100.         super.dispatchDraw(canvas) ;  
  101.     }  
  102.       
  103.     protected boolean drawChild(Canvas canvas , View child, long drawingTime){  
  104.         Log.i(TAG, "**** drawChild start ****") ;  
  105.           
  106.         return super.drawChild(canvas, child, drawingTime) ;  
  107.     }  
  108. }  

   

          2、MyView.java 自定义View类型,重写onDraw()方法 ,

[java] view plaincopyprint?
  1. //自定义View对象  
  2.     public class MyView extends View{  
  3.   
  4.         private Paint paint  = new Paint() ;  
  5.           
  6.         public MyView(Context context) {  
  7.             super(context);  
  8.             // TODO Auto-generated constructor stub  
  9.         }  
  10.         public MyView(Context context , AttributeSet attrs){  
  11.             super(context,attrs);  
  12.         }  
  13.           
  14.         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){  
  15.             //设置该View大小为 80 80  
  16.             setMeasuredDimension(50 , 50) ;  
  17.         }  
  18.           
  19.           
  20.           
  21.         //存在canvas对象,即存在默认的显示区域  
  22.         @Override  
  23.         public void onDraw(Canvas canvas) {  
  24.             // TODO Auto-generated method stub  
  25.             super.onDraw(canvas);  
  26.               
  27.             Log.i("MyViewGroup""MyView is onDraw ") ;  
  28.             //加粗  
  29.             paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));  
  30.             paint.setColor(Color.RED);  
  31.             canvas.drawColor(Color.BLUE) ;  
  32.             canvas.drawRect(003030, paint);  
  33.             canvas.drawText("MyView"1040, paint);  
  34.         }  
  35.     }  


          主Activity只是显示了该xml文件,在此也不罗嗦了。 大家可以查看该ViewGroup的Log仔细分析下View的绘制流程以及

相关方法的使用。第一次启动后捕获的Log如下,网上找了些资料,第一次View树绘制过程会走几遍,具体原因可能是某些

View 发生了改变,请求重新绘制,但这根本不影响我们的界面显示效果 。

 

        总的来说: 整个绘制过程还是十分十分复杂地,每个具体方法的实现都是我辈难以立即的,感到悲剧啊。对Android提

 供的一些ViewGroup对象,比如LinearLayout、RelativeLayout布局对象的实现也很有压力。 本文重在介绍整个View树的绘制

流程,希望大家在此基础上,多接触源代码进行更深入地扩展。

 

 

      

       示例DEMO下载地址:http://download.csdn.net/detail/qinjuning/3982468

 

//==========================================================

// 本次更新于 2012-05-20 晚

//==========================================================


 Al Last,关于UI绘制的这块,我博客里零零散散的叙说了一些知识,建议大家都能够去看看:


   1、  详解measure过程以及如何设置View宽高的,建议看我的另外两篇博客:

         <<Android中measure过程、WRAP_CONTENT详解以及xml布局文件解析流程浅析(上)>>

         <<Android中measure过程、WRAP_CONTENT详解以及xml布局文件解析流程浅析(下)>>


  2、详解DecorView以及Activity窗口对应布局地说明

      <<Android中将布局文件/View添加至窗口过程分析 ---- 从setContentView()谈起>>


  3、详解View绘制过程中如何绘制背景图片:

       <<Android中View(视图)绘制不同状态背景图片原理深入分析以及StateListDrawable使用详解>>

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 被买家提供证明说我卖假货怎么办 淘宝退货快递公司填错了俩次怎么办 淘宝上退货把运单号写错了怎么办? 阿里巴巴发货了快递单号掉了怎么办 淘宝上买东西货物被物流扣留怎么办 淘宝买错了对方已发货怎么办 淘宝卖家发货的时候没货了怎么办 买了球显示未出票中奖了怎么办 点错了允许易企秀获得权限怎么办 淘宝还没发货商家拒绝退款怎么办 买家不补邮费还要你发货怎么办 天猫客服提示获取信息失败怎么办 拼多多下单了商家不发货怎么办 店铺扣2分宝贝降权了怎么办 淘宝店没订单没流量怎么办啊 淘宝id账号登录密码忘记了怎么办 淘宝网店铺授权客服联系不上怎么办 卖家发货买家查不到物流信息怎么办 物流信息到了但东西没到怎么办 淘宝详情已更改对方恶意投诉怎么办 淘宝店写的不给退换怎么办 从饿了么商家借款逾期怎么办 谷歌浏览器网页无法复制文字怎么办 复制粘贴在文件本里找不到了怎么办 淘宝商家给我寄错东西怎么办 美团客户点餐商家没有了怎么办 如果银行卡里的钱突然没有了怎么办 微信绑定的银行卡怎么没有了怎么办 西亚超市商品退货赠品不退怎么办 淘宝发布宝贝推荐橱窗位不够怎么办 抖音和微信软件冲突怎么办 淘宝开过店想给别家做客服怎么办 学网上开店别人不主动教你怎么办 淘宝店铺身份信息复核逾期了怎么办 一件代发进货后如果没卖出去怎么办 国外供货商收了钱不发货怎么办 企业被注销之后淘宝企业店怎么办 在淘宝上买了东西店家关门了怎么办 在淘宝上买东西被店家骂了怎么办 淘宝店家错发了两次货怎么办 我的身份证注销了淘宝支付宝怎么办