)##介绍

在Android开发过程中,经常存在需要实现自定义控件的情况,对于比较简单的需求,通过组合系统提供的原生控件既可以完成,但是一旦碰到比较复杂的控件时候,这时候就需要我们亲自动手完成控件的设计,实现对控件的测量、布局、绘制等操作,而这一且操作的前提是你需要了解并掌握View的绘制流程。

在正式讲解View的绘制流程之前,我们有必要先来简单了解下Android的UI管理系统层级关系,如下图所示:

PhoneWindow 是Android系统中最基本的窗口系统,每一个Activity会创建一个。PhoneWindow是Activity和View系统交互的接口。DecorView本质上是一个FrameLayout,是Activity中所有View的祖先。

绘制整体流程

当一个应用启动的时候,会启动一个主Activity,Android系统会根据Activity的布局来对它进行绘制。绘制会从根视图ViewRootImpl的performTraversals()方法开始,从上到下遍历整个视图树,每一个View控件负责绘制自己,而ViewGroup还需要负责通知自己的子View进行绘制操作。整个流程如下图所示:

视图的绘制可以分为三个步骤,分别为测量(Measure)、布局(Layout)和绘制(Draw)。

private void performTraversals() {    int childWidthMeasureSpec=getRootMeasureSpec(mWidth,lp.width);    int childHeightMeasureSpec=getRootMeasureSpec(mHeight,lp.height);    .........    //执行测量流程    performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);    .........    //执行布局流程    performLayout(lp,desiredWindowWidth,desiredWindowHeight);    .........    //执行绘制流程    performDraw();}

其框架过程如下:

MeasureSpec

在介绍Measure过程之前,我们先了解一下MeasureSpec,这对之后理解Measure过程是十分重要的!
MeasureSpec表示的是一个32位整型值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec是View类的一个静态内部类,用来说明如何测量这个View,其核心代码如下

 public static class MeasureSpec {        private static final int MODE_SHIFT = 30;        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})        @Retention(RetentionPolicy.SOURCE)        public @interface MeasureSpecMode {}        //不指定测量模式        public static final int UNSPECIFIED = 0 << MODE_SHIFT;        //精确测量模式        public static final int EXACTLY     = 1 << MODE_SHIFT;        //最大值测量模式        public static final int AT_MOST     = 2 << MODE_SHIFT;        //根据指定的大小和模式创建一个MeasureSpec        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,                                          @MeasureSpecMode int mode) {            if (sUseBrokenMakeMeasureSpec) {                return size + mode;            } else {                return (size & ~MODE_MASK) | (mode & MODE_MASK);            }        }        public static int makeSafeMeasureSpec(int size, int mode) {            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {                return 0;            }            return makeMeasureSpec(size, mode);        }        //获取测量模式        @MeasureSpecMode        public static int getMode(int measureSpec) {            //noinspection ResourceType            return (measureSpec & MODE_MASK);        }       //获取测量大小        public static int getSize(int measureSpec) {            return (measureSpec & ~MODE_MASK);        }        //微调某一个MeasureSpec的大小        static int adjust(int measureSpec, int delta) {            final int mode = getMode(measureSpec);            int size = getSize(measureSpec);            if (mode == UNSPECIFIED) {                // No need to adjust size for UNSPECIFIED mode.                return makeMeasureSpec(size, UNSPECIFIED);            }            size += delta;            if (size < 0) {                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +                        ") spec: " + toString(measureSpec) + " delta: " + delta);                size = 0;            }            return makeMeasureSpec(size, mode);        }    }

小结(重点关注代码中的以下三种测量模式):

  • 1、UNSPECIFIED:不指定测量模式,父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。
  • 2、EXACTLY:精确测量模式,当该视图的layout_width或者layout_height指定为具体数值或者match_parent时生效,表示父视图已经决定了子视图的精确大小,这种模式下的View测量值就是SpecSize大小的值。
  • 3、AT_MOST:最大值模式,当该视图的layout_width或者layout_height指定为wrap_content时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。

注意:对DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。

Measure过程

主要作用:

  • 为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。

由上面我们知道,页面的测量流程是从performMeasure()方法开始的,核心代码如下

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {        ..........        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);        .......... }

从上面可以看出。具体的测量操作是分发给ViewGroup的,由ViewGroup在它的measureChild方法中传递

measureChildren主要是遍历ViewGroup中的所有View进行测量代码如下

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {        final int size = mChildrenCount;        final View[] children = mChildren;        for (int i = 0; i < size; ++i) {            final View child = children[i];            // 当View的可见性处于Gone状态时,不对其进行测量            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {                measureChild(child, widthMeasureSpec, heightMeasureSpec);            }        }    }

measureChild是为了测量某一个指定的View 重要根据父容器的MeasureSpec和子View的LayoutParams等信息计算子View的MeasureSpec

protected void measureChild(View child, int parentWidthMeasureSpec,            int parentHeightMeasureSpec) {        final LayoutParams lp = child.getLayoutParams();        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,                mPaddingLeft + mPaddingRight, lp.width);        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,                mPaddingTop + mPaddingBottom, lp.height);        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    }

measure函数原型为 View.java 该函数不能被重载,而是通过回调onMeasure方法实现的。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {    .........    onMeasure(widthMeasureSpec,heightMeasureSpec);    ......... }

onMeasure方法通常是由View特定子类自己实现的,开发者也可以通过重写这个方法实现自定义View。

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

//如果View没有重写onMeasure方法,则会默认直接调用getDefaultSize来获取View的宽高

public static int getDefaultSize(int size, int measureSpec) {        int result = size;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        switch (specMode) {        case MeasureSpec.UNSPECIFIED:            result = size;            break;        case MeasureSpec.AT_MOST:        case MeasureSpec.EXACTLY:            result = specSize;            break;        }        return result; }

小结:

具体的调用链如下:

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调用流程就是个树形的递归过程。

为了大家更好的理解采用下面的伪代码

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

Layout过程

主要作用 :
为将整个根据子视图的大小以及布局参数将View树放到合适的位置上。该过程用来确定View在父容器中的布局位置。

由父容器获取子View的位置参数后,调用子View的layout方法并将位置参数传入实现的,ViewRootImpl的performLayout代码如下:

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,            int desiredWindowHeight) {    ........    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());    ........}
//View.javapublic void layout(int l, int t, int r, int b) {boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴   ........ onLayout(changed, l, t, r, b); ........}
// 空方法,子类如果是ViewGroup类型,则重写这个方法,实现ViewGroup中所有View控件布局流程protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}

具体的调用链如下:

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

1、layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ;
2、如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。

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

    // layout()过程  ViewRoot.java      // 发起layout()的"发号者"在ViewRoot.java里的performTraversals()方法, mView.layout()      private void  performTraversals(){          //...          View mView  ;             mView.layout(left,top,right,bottom) ;          //....      }      //回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现      private void onLayout(int left , int top , right , bottom){       //如果该View不是ViewGroup类型       //调用setFrame()方法设置该控件的在父视图上的坐标轴       setFrame(l ,t , r ,b) ;       //--------------------------       //如果该View是ViewGroup类型,则对它的每个子View进行layout()过程       int childCount = getChildCount() ;       for(int i=0 ;i//2.1、获得每个子View对象引用        View child = getChildAt(i) ;        //整个layout()过程就是个递归过程        child.layout(l, t, r, b) ;       }     }  

Draw 过程

Draw操作用来将控件绘制出来,绘制的流程是从performDraw方法开始,核心代码如下。

private void performDraw(){   ......   draw(fullRedrawNeeded);   ......}private void draw(boolean fullRedrawNeeded) {........if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {        return;    }........}private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,            boolean scalingRequired, Rect dirty) {....... mView.draw(canvas);.......}

可以看到最终调用到每一个View的Draw方法绘制每一个具体的View,绘制基本上可以分为六个步骤,代码如下

public void draw(Canvas canvas) {    ........   //步骤一:绘制View的背景   drawBackground(canvas);   .........   //步骤二:如果需要的话,保存canvans的图层,为fading做准备   saveCount = canvas.getSaveCount();   ........   canvas.saveLayer(left, top, right, top + length, null, flags);   //步骤三:绘制View的内容   onDraw(canvas);   .....   //步骤四:绘制View的子View    dispatchDraw(canvas);//步骤五:如果需要的话,绘制View的fading边缘并恢复图层    canvas.drawRect(right - length, top, right, bottom, p);    ......   canvas.restoreToCount(saveCount);    ......  //步骤六:绘制View的装饰(比如滚动条)  onDrawScrollBars(canvas);}

小结

由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()方法)。

伪代码:

    // draw()过程     ViewRoot.java      // 发起draw()的"发号者"在ViewRoot.java里的performTraversals()方法, 该方法会继续调用draw()方法开始绘图      private void  draw(){          //...       View mView  ;          mView.draw(canvas) ;            //....      }      //回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现      private void draw(Canvas canvas){       //该方法会做如下事情       //1 、绘制该View的背景       //2、为绘制渐变框做一些准备操作       //3、调用onDraw()方法绘制视图本身       //4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。            // 应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。       //5、绘制渐变框        }      //ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法      @Override      protected void dispatchDraw(Canvas canvas) {       //        //其实现方法类似如下:       int childCount = getChildCount() ;       for(int i=0 ;i//调用drawChild完成        drawChild(child,canvas) ;       }           }      //ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法      protected void drawChild(View child,Canvas canvas) {       // ....       //简单的回调View对象的draw()方法,递归就这么产生了。       child.draw(canvas) ;       //.........      }  

强调一点的就是,在这三个流程中,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()方法

说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括该调用者本身。

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

  • 1、setVisibility()方法:当View的可视状态在INVISIBLE/ VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。

requestFocus()方法

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

总结

至此View绘制流程基本讲述完毕,为了更好的巩固这些知识,可以参考我的另一篇文章Android中自定义控件之流式布局实现方式

更多相关文章

  1. 增加 Andorid手機電池續航力 ,一定有效的省電心得
  2. Android(安卓)Google Map API使用的八个步骤
  3. 不同手机在豌豆夹上显示设备同名的解决方法 && Android(安卓)获
  4. 2016年末,Android岗位BAT等大厂面试题知识点小结(一)Android基础部
  5. 关于android各种双卡手机获取imei,imsi的处理(mtk,展讯,高通等)
  6. Android自定义View(五)——带扫描线的View
  7. 【Android应用开发】-(21)Android中巧用反射解决程序兼容性问题
  8. Android(安卓)OpenGLES2.0(四)——正方形和圆形
  9. [置顶] High Performance Canvas Game for Android(高性能Android

随机推荐

  1. android webview中js交互、第三方分享。
  2. textview中自动换行显示文本内容
  3. Android中音乐文件的信息详解【安卓源码
  4. Android中retrofit网络请求框架使用
  5. 由Android4.4收起写sdcard文件权限想到的
  6. 9款Android常用的快速开发框架
  7. Android高手进阶教程(七)之----Android(
  8. Android消息推送
  9. android hidl简单实例1
  10. android TV广播监听usb和U盘的挂载