view的工作原理

view的工作原理

  • 基本概念

    1. ViewRoot
      对应ViewRootImpl类 是连接WindowManager和DecorView的纽带,view的三大流程均通过ViewRoot来完成,在ActivityThread中,当Activity对象呗创建完毕后,会将DectorView添加到window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DectorView对象进行关联,参照如下源码

      root = new ViewRootImpl(view.getContext(),display);
      root.setView(view,wparams,panelParentView());
      

      View的绘制流程是从view的performTraversals(译为进行遍历)方法开始的,他经过measure layout draw三个过程才能,最终将一个view绘制出来,其中measure用来测量View的宽高 layout用来确定view在父容器中的放置位置,draw则负责将view绘制在屏幕上,performTraversals会依次调用performMeasure performLayout performDraw三个方法 这三个方法分别完成顶级view的measure layout draw这三大流程

      其中在performMeasure方法中又会调用measure方法,在measure方法中又会调用onMeasure方法 在onMeasure方法中则会对所有的子元素进行measure过程,这时候measure流程就从父元素中传递到子元素中了 这样就完成了一次measure过程 接着子元素同样会重复这个过程 如此反复直到完成整个viewTree的遍历

      同理 performLayout和performDraw的传递流程也是类似的 唯一的不同是performDraw的传递过程是在draw方法中调用dispatchDraw来实现的 本质上并没有区别

      measure过程决定了view的宽高,measure完成之后, 可以通过getMeasureWidth和getMeasureHeight方法来获得view测量之后的宽高,几乎所有的情况下都是正确的,但是有例外,后边会详细说明,layout过程决定了view四个顶点的坐标和view实际的宽高,完成之后,可以通过getTop、getBottom、getLeft和getRight来拿到view四个顶点的位置,并可以通过getWidth和getHeight来拿到view最终的宽高,draw过程则决定了view的显示 只有draw方法完成之后view的内容才能呈现在屏幕上

    2. DectorView
      作为顶级view 一般情况下来说他的内部会包含一个竖直方向的LinearLayout,在这个LinearLayout中分为上下两个部分,上边是标题栏,下边是内容栏,在Activity中我们通过setContentView所设置的布局文件就是被添加到内容栏中的 由此可以解释,为什么在activity中指定布局的方法不叫setView而是叫setContentView ,我们添加的布局被添加到id为content的FrameLayout中去了,可以通过如下代码来获得content和我们所设置的布局

      ViewGrounp content = (ViewGrounp)findViewById(android.R.id.content);
      content.getChildAt(0);
      
    3. MeasureSpec
      译为测量规格,MeasureSpec代表了一个32位的int值,高2位代表SpecMode,低30位代表了SpecSize,SpecMode表示测量模式,SpecSize表示在某种测量模式之下的规格大小,MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的内存占用 为了方便操作 系统提供了打包和解包方法

      • SpecMode有三个类 如下所示

        UNSPECIFIED 未指明的
            父容器不对View有任何限制 要多大给多大 一般多用于系统内部 表示一种测量状态 不需过多纠结
        
        EXACTLY 确切的
            父容器已经检测出View所需的精确大小 这个时候view的最终大小就是SpecSize所指定的值 对应于LayoutParams中的match_parent和具体数值两种模式
        
        AT_MOST 最大范围之内
            父容器指定了一个可用大小 即SpecSize view的大小不能超过这个值 对应于LayoutParams中的wrap_content
        
      • MeasureSpec和LayoutParams的对应关系

        对于等级view(DectorView)和普通view来说 MeasureSpec的转换过程略有不同,对于DectorView 其MeasureSpec,由窗口的尺寸和其自身的LayoutParams决定,对于普通view,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定view的测量宽高

      • DectorView的MeasureSpec的产生遵循如下规则

        LayoutParams.MATCH_PARENT:精确模式 大小就是窗口的大小
        LayoutParams.WRAP_CONTENT:最大模式 大小不定 但是不能超过窗口的大小
        固定大小(如:100dp) 精确模式 大小为LayoutParams中指定的大小

        对于普通view来说 由于他的MeasureSpec受父容器的MeasureSpec影响 所以情况要比较复杂 源码内容也比较多,所以我们借助一个表格来表示其规则(其中parentSize是指父容器中目前可以使用的空间)

        parentSpecMode
        childLayoutParams
        EXACTLY AT_MOST UNSPECIFIED
        dp/px EXACTLY
        childSize
        EXACTLY
        childSize
        EXACTLY
        childSize
        match_parent EXACTLY
        parentSize
        AT_MOST
        parentSize
        UNSPECIFIED
        0
        wrap_content AT_MOST
        parentSize
        AT_MOST
        parentSize
        UNSPECIFIED
        0

        总结一下:
        1.当view使用固定宽高的时候,不管父容器的SpecMode是什么,view的MeasureSpec都是精确模式并且其大小遵循自身的LayoutParams中的大小
        2.当view使用match_parent时,如果其父容器是精确模式,那么view也会是精确模式,并且使用父容器中可用的宽高,如果父容器是最大模式,那么view也将是最大模式,并且使用父容器中剩余的宽高
        3.当view使用wrap_content时,不管父容器是什么模式,view的MeasureSpec都是最大模式,并且其大小会使用父容器剩余可用的大小

        注:最后一个多用于系统内部 可以忽略不做过多关注

  • View的工作流程

    view的工作流程主要包括measure layout draw三大流程,即测量、布局和绘制,其中measure确定view的测量宽高,layout确定view的最终宽高和四个顶点的实际位置 ,draw则将view绘制到屏幕上

    • measure过程
      measure过程是三大流程中比较复杂的一个,要分情况来看,如果只是一个view,那么measure过程就是完成了其测量过程,如果是一个ViewGrounp,除了要完成自己的测量过程之外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程 直到测量完成

      1. view的measure过程

        view的measure过程是尤其measure方法来完成的,measure方法是一个final方法,无法重写,在view的measure方法中又会调用view的onMeasure方法,通过阅读view的onMeasure方法可知,view的宽高由specSize决定,由此我们可以得出以下结论:

        直接继承view的自定义控件需要重写onMeasure方法,并设置wrap_content时的自身大小,否则在局中使用wrap_content就相当于使用match_parent
        

        为什么呢?如果我们在view中使用wrap_content时,那么view的specMode是AT_MOST,在这种模式之下他的宽高等于specSize,而我们在前边阅读源码的过程中,总结过一个表格,查表格可知,这种情况下,不管父容器是那种SpecMode,view的SpecSize始终都会是parentSize,也就是说,会占用父容器剩余的所有空间,这种效果和使用match_parent有什么区别呢?

        查看系统的TextView和ImageView等控件的源码可知 他们针对这种情况也做出了对应的处理 在onMeasure方法中针对wrap_content做出了特殊处理 我们在自己的控件中也可以类似的解决 如下所示

        protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
           super.onMeasure(widthMeasureSpec,heightMeasureSpec);
           int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
           int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
           int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
           int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
           if(widthSpecMode == MeasureSpec.AT_MOST&&heightSpecMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(mWidth,mHeight);
           }else if(widthSpecMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(mWith,heightSpecSize);
           }else if(heightSpecMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(widthSpecSize,meight);
           }
         }
        

        在上边代码中 我们只需要给view指定一个默认的内部宽高(mWidth和mHeight)并在使用wrap_content时对view设置这个宽高即可

      2. ViewGrounp的measure过程
        对于ViewGrounp来说,除了完成自己的measure过程之外,还需要遍历子元素,调用所有子元素的measure方法,各个子元素再递归去指向这个过程,直到measure完成,和view不同的是,ViewGrounp是一个抽象类,因此他并没有重写view的onMeasure方法,而是提供了一个叫measureChildren的方法,来遍历各个子元素,调用子类的measure方法,我们直到 ViewGrounp并没有定义其测量的具体过程,因为ViewGrounp是一个抽象类,其测量过程中的onMeasure方法需要各个子类去自己实现 因为各个ViewGrounp有截然不同的特性,所以ViewGrounp没办法像view意向对onMeasure方法进行统一,只能由子类根据自身特性来去实现

      3. 如何在Activity中去获取某个view的测量宽高
        在Activity中我们无法在onCreate onStart onResume等生命周期中去获取某一个view的测量宽高,因为View的measure过程和Activity的生命周期方法并不是同步的,因此无法保证Activity执行了这些生命周期方法时某个view一定已经测量完毕了,如果view此时还没有测量完毕,那么我们所获得的宽高就是0,所以我们需要另外的方法,来获取view的measure宽高

        1. Activity/View#onWindowFocusChanged
          onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,这时候去获取某个view的测量宽高是没有问题的,但是需要注意的是,onWindowFocusChanged方法在,Activity的窗口得到和失去焦点的时候均会被调用一次,也就是说,如果频繁的调用onPause和onResume方法的话,该方法会频繁的被调用,典型代码如下:

          public void onWindowFocusChanged(boolean hasFocus){
              super.onWindowFocusChanged(hasFocus);
                  if(hasFocus){
                     int width = view.getMeasuredWidth();
                     int height = view.getMeasuredHeight();
                  }
          }
          
        2. view.post(runnable)
          通过post可以将一个runnable投递到view消息队列的尾部 然后等待Looper调用次runnable时,view的其他任务已经完成 view已经初始化好了 典型代码如下

          protected void onStart(){
              super.onStart();
                  view.post(new Runnable(){
                      @override
                      public void run(){
                          int width = view.getMeasuredWidth();
                          int height = view.getMeasuredHeight();
                  }
              });
           }
          
        3. ViewTreeObserver
          使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener
          这个接口,当ViewTree的状态发生改变或者viewTree内部的View的可见性发生改变的时候
          onGlobalLayoutListener方法会被回调,因此我们可以在这个时候获取view的measure宽高
          需要注意的是,伴随着viewTree的状态改变 该方法会被回调多次 典型代码如下:

          protected void onStart(){
             super.onStart();
             ViewTreeObserver observer = view.getViewTreeObserver();
             observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
                  @suppressWarnings("deprecation")
                  @override
                  public void onGlobalLayout(){
                      view.getViewTreeObserver.removeGlobalLayoutListener(this);
                      int width = view.getMeasuredWidth();
                      int height = view.getMeasuredHeight();
                  }
             });
           }
          
        4. view.measure(int widthMeasureSpec,int hegihtMeasureSpec)
          通过手动的对view进行measure来得到view的measure宽高 这种方法比较复杂 需要分情况具体处理,根据view的LayoutParams来分 而且容易出现错误用法 不推荐使用

    • layout过程
      layout的作用是ViewGrounp用来确定子元素的位置,当viewGrounp的位置被确定之后 他在onLayout中会遍历所有的子元素,并调用子元素的layout方法,子元素确定自身位置之后,又会调用子元素的onLayout方法来确定其子元素的位置,以此类推,直到viewTree绘制完毕,layout方法确定view本身的位置 onLayout方法确定view子元素的位置

      layout方法的大致流程如下,首先会通过setFrame方法来设定view的四个顶点的位置,即初始化mLeft mTop mBottom mRight这四个值,view的四个顶点一旦被确定,view在父容器中的位置也就确定了,接着会调用onLayout方法,这个方法的用途是确定子元素的位置,在onLayout方法中,会调用setChildFrame,在该方法中又会调用子元素的layout方法,这样父元素在layout方法中确定自身的位置之后,就通过onLayout方法去调用子元素的layout方法,子元素又会通过layout来确定自己的位置,层层传递,直到ViewTree绘制完毕

    • Draw过程
      阅读源码可知 View的绘制过程遵循以下几个步骤:

      1. 绘制背景 background.draw(canvas)
      2. 绘制自身 onDraw(canvas)
      3. 绘制children dispatchDraw(canvas)
      4. 绘制装饰 onDrawScrollBars(canvas)

    View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有元素的draw方法 如此draw,事件就层层传递下去,直到绘制完毕.

  • 自定义View

    自定义View的分类

    1. 继承View重写onDraw方法
      主要用于实现一些不规则的效果,采用这种方法需要注意的就是需要自己支持wrap_content和padding,如果不定义wrap_content 在布局文件中使用该效果,回合match_parent一样(具体原因前边已做分析) 同时 view的padding属性需要在onDraw方法中去实现,否则没有效果

    2. 继承ViewGrounp派生出特殊的layout
      主要用于实现自定义布局,需要注意合适的处理viewGrounp的测量,布局过程,并且同时处理子元素的测量和布局.

    3. 继承特定的view(如TextView等)
      主要用于对某种已有的view的功能进行拓展,该方法比较容易实现,不需要自己去处理wrap_content和padding

    4. 继承特定的ViewGrounp(如LinearLayout等)
      与2类似,同时又不需要自己处理ViewGrounp的测量和绘制过程

    自定义View需要注意的问题

    1. 让View支持wrap_content
      直接继承view或者viewGrounp的控件,需要在onMeasure中对wrap_content做特殊处理

    2. 如果有必要 让自己的view支持padding
      直接继承view的控件,如果不在draw方法中处理padding,那么padding属性将失效,直接继承ViewGrounp的控件,需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响 不然将导致padding和子元素的margin属性失效

    3. 尽量不要在view中使用Handler
      view内部本身提供了post系列方法 完全可以替代Hnadler的作用

    4. View中如果有线程和动画需要停止 参照View#onDetachedFromWindow,当包含这个View的Activity退出或者当前view被remove时,View的onDetachedFromWindow方法会被调用,我们可以在这个回调中停止线程或者动画,与此对应的是onAttachedToWindow方法,当包含这个view的Activity启动时,View的onAttchedToWindow方法会被调用,同时,当View不可见是我们也需要停止线程和动画

    5. View带有滑动嵌套时 需要处理好滑动冲突
      具体请参见博客:view基础知识介绍(二)

参考资料:Android开发艺术探索

你可能感兴趣的:(Android)