详解View的绘制流程

写在开头

此篇为学习总结文章,加入自己的一些对学习博文的纠正和学习心得。借此记录和大家一起学习,有不同意见还请大家一起讨论。

谁负责View的绘制?

ViewRoot是android用来负责执行View绘制的整个流程的。实际上,View的绘制是由ViewRoot来负责,具体由ViewRootImpl类来实现。在android中,每个应用程序窗口的DecorView都有一个与之关联的ViewRoot对象(ViewRootImpl),这种关联关系是由WindowManager来维护的(windowManagerImpl实现)。在Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。

View绘制的源头

我们还要从Activity在第一次setContentView时说起,其实这步操作做的操作是初始化各个view对象, 并制定上下级关系,仅此而已。真正开始的地方是ActivityThread.handleResumeActivity中通过windowManager的实现类windowManagerImpl的addView方法,方法内部通过ViewRootImpl的实例调用setView方法。该方法内部的requestlayout方法中会执行scheduleTraversals()方法,在方法中会通过内部的Handler发出消息,开始执行doTraversal,从而执行performTraversals(),View的绘制流程正式开始。

    //ViewRootImpl类部分源码
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
    }


    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();//检查是否在主线程
            mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
            scheduleTraversals();
        }
    }


    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

            //post一个runnable处理-->mTraversalRunnable
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ``````//省略
        }
    }

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    void doTraversal() {
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            performTraversals();//View的绘制流程正式开始。
    }

View绘制的主要流程

View的绘制主要流程分为三步,measure、layout和draw。整个View树的绘图流程是在ViewRootImpl类的performTraversals()方法(这个方法巨长)开始的,该函数做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小(measure)、是否重新放置视图的位置(layout)、以及是否重绘 (draw),其核心也就是通过判断来选择顺序执行这三个方法中的哪个。下面看源码(ViewRootImpl类中):

注:此段源码会说明一问题,为什么我们App看到的视图都是全屏。

    private void performTraversals() {
        ......
        //最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
        //lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ......
        //mView就是我们的DecorVIew
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
        ......
        mView.draw(canvas);
        ......
    }
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
    //上面传入参数后这个函数走的是MATCH_PARENT,使用MeasureSpec.makeMeasureSpec方法组装一个MeasureSpec,MeasureSpec的specMode等于EXACTLY,specSize等于windowSize,也就是为何根视图总是全屏的原因。
        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        ......
        }
        return measureSpec;
    }

总而言之,performTraversals就是做了这些事情:

这里写图片描述

measure

要理解measure,首先要理解下MeasureSpec:

MeasureSpec决定了一个View的尺寸规格,并且View的MeasureSpec受自身的LayoutParams(一般是xml布局中width和height)和父容器MeasureSpec的影响。MeasureSpec又由SpecMode 和SpecSize 组成。SpecMode 是测量模式,有UNSPECIFIED(父容器对子View的尺寸不作限制,由子View自己测量)、 EXACTLY( SpecSize 表示View的最终大小,因为父容器已经检测出View所需要的精确大小,它对应LayoutParams中的match_parent和具体的数值这两种模式)、AT_MOST(SpecSize 表示父容器的可用大小,View的大小不能大于这个值。它对应LayoutParams中的wrap_content。)三种。SpecSize是在某种测量模式下的尺寸和大小。

整个View树的源码measure流程图(引用自凶残的程序员)

这里写图片描述

扫描二维码关注公众号,回复: 1023685 查看本文章

Measyre阶段的目的是计算出控件树中的各个控件要显示其内容需要多大尺寸。起点是ViewRootImpl的measureHierarchy()方法,这个方法的源码如下:

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;
        ···
        boolean goodMeasure = false;
        ···
        if (!goodMeasure) {
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }
    ···
        return windowSizeMayChange;
    }

上面的代码中调用getRootMeasureSpec()方法来获取根MeasureSpec,这个根MeasureSpec代表了对decorView的宽高的约束信息。所以在一开始传入performMeasure()方法的MeasureSpec的SpecMode为EXACTLY,SpecSize为窗口尺寸。接下来执行的是performMeasure方法。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
  . . .
  try { 
   //调用DecorView的mesure方法。
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  } finally {
    . . .
  }
}

接下来会调用到VIew的meaure方法,在measure()方法中,会决定是否进行重新测量的工作(源码不贴了)如下:

   //final方法,子类不可重写。每个View控件的实际宽高都是由父视图和自身决定的。实际的测量是在onMeasure方法进行,所以在View的子类需要重写onMeasure方法,这是因为measure方法是final的,不允许重载,所以View子类只能通过重载onMeasure来实现自己的测量逻辑。(结合自定义View想想)
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        //回调onMeasure()方法
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
    }

实际测量工作的是LinearLayout.FrameLayout的onMeasure()方法。DecorView是一个LinearLayout其实。

     //View的onMeasure默认实现方法
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

FrameLayout是ViewGroup的子类,后者有一个View[]类型的成员变量mChildren,代表了其子View集合。通过getChildAt(i)能获取指定索引处的子View,通过getChildCount()可以获得子View的总数。

在上面的源码中,首先调用measureChildWithMargins()方法对所有子View进行了一遍测量,并计算出所有子View的最大宽度和最大高度。而后将得到的最大高度和宽度加上padding,这里的padding包括了父View的padding和前景区域的padding。然后会检查是否设置了最小宽高,并与其比较,将两者中较大的设为最终的最大宽高。最后,若设置了前景图像,我们还要检查前景图像的最小宽高。

经过了以上一系列步骤后,我们就得到了maxHeight和maxWidth的最终值,表示当前容器View用这个尺寸就能够正常显示其所有子View(同时考虑了padding和margin)。而后我们需要调用resolveSizeAndState()方法来结合传来的MeasureSpec来获取最终的测量宽高,并保存到mMeasuredWidth与mMeasuredHeight成员变量中。

从以上代码的执行流程中,我们可以看到,容器View通过measureChildWithMargins()方法对所有子View进行测量后,才能得到自身的测量结果。也就是说,对于ViewGroup及其子类来说,要先完成子View的测量,再进行自身的测量(考虑进padding等)。
接下来我们来看下ViewGroup的measureChildWithMargins()方法的实现:

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        //获取子视图的LayoutParams
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //调整MeasureSpec
        //通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //调运子View的measure方法,子View的measure中会回调子View的onMeasure方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

由以上代码我们可以知道,对于ViewGroup来说,它会调用child.measure()来完成子View的测量。传入ViewGroup的MeasureSpec是它的父View用于约束其测量的,那么ViewGroup本身也需要生成一个childMeasureSpec来限制它的子View的测量工作。这个childMeasureSpec就由getChildMeasureSpec()方法生成。接下来我们来分析这个方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
  // spec为父View的MeasureSpec
  // padding为父View在相应方向的已用尺寸加上父View的padding和子View的margin
  // childDimension为子View的LayoutParams的值
  int specMode = MeasureSpec.getMode(spec);
  int specSize = MeasureSpec.getSize(spec);

  // 现在size的值为父View相应方向上的可用大小
  int size = Math.max(0, specSize - padding);

  int resultSize = 0;
  int resultMode = 0;

  switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
      if (childDimension >= 0) {
        // 表示子View的LayoutParams指定了具体大小值(xx dp)
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // 子View想和父View一样大
        resultSize = size;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // 子View想自己决定其尺寸,但不能比父View大 
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      }
      break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
      if (childDimension >= 0) {
        // 子View指定了具体大小
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // 子View想跟父View一样大,但是父View的大小未固定下来
        // 所以指定约束子View不能比父View大
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // 子View想要自己决定尺寸,但不能比父View大
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      }
      break;

      . . .
  }

  //noinspection ResourceType
  return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上面的方法展现了根据父View的MeasureSpec和子View的LayoutParams生成子View的MeasureSpec的过程, 子View的LayoutParams表示了子View的期待大小。这个产生的MeasureSpec用于指导子View自身的测量结果的确定。
在上面的代码中,我们可以看到当ParentMeasureSpec的SpecMode为EXACTLY时,表示父View对子View指定了确切的宽高限制。此时根据子View的LayoutParams的不同,分以下三种情况:

具体大小(childDimension):这种情况下令子View的SpecSize为childDimension,即子View在LayoutParams指定的具体大小值;令子View的SpecMode为EXACTLY,即这种情况下若该子View为容器View,它也有能力给其子View指定确切的宽高限制(子View只能在这个宽高范围内),若为普通View,它的最终测量大小就为childDimension。
match_parent:此时表示子View想和父View一样大。这种情况下得到的子View的SpecMode与上种情况相同,只不过SpecSize为size,即父View的剩余可用大小。
wrap_content: 这表示了子View想自己决定自己的尺寸(根据其内容的大小动态决定)。这种情况下子View的确切测量大小只能在其本身的onMeasure()方法中计算得出,父View此时无从知晓。所以暂时将子View的SpecSize设为size(父View的剩余大小);令子View的SpecMode为AT_MOST,表示了若子View为ViewGroup,它没有能力给其子View指定确切的宽高限制,毕竟它本身的测量宽高还悬而未定。

当ParentMeasureSpec的SpecMode为AT_MOST时,我们也可以根据子View的LayoutParams的不同来分三种情况讨论:

具体大小:这时令子View的SpecSize为childDimension,SpecMode为EXACTLY。
match_parent:表示子View想和父View一样大,故令子View的SpecSize为size,但是由于父View本身的测量宽高还无从确定,所以只是暂时令子View的测量结果为父View目前的可用大小。这时令子View的SpecMode为AT_MOST。
wrap_content:表示子View想自己决定大小(根据其内容动态确定)。然而这时父View还无法确定其自身的测量宽高,所以暂时令子View的SpecSize为size,SpecMode为AT_MOST。

从上面的分析我们可以得到一个通用的结论,当子View的测量结果能够确定时,子View的SpecMode就为EXACTLY;当子View的测量结果还不能确定(只是暂时设为某个值)时,子View的SpecMode为AT_MOST。

在measureChildWithMargins()方法中,获取了知道子View测量的MeasureSpec后,接下来就要调用child.measure()方法,并把获取到的childMeasureSpec传入。这时便又会调用onMeasure()方法,若此时的子View为ViewGroup的子类,便会调用相应容器类的onMeasure()方法,其他容器View的onMeasure()方法与FrameLayout的onMeasure()方法执行过程相似。

下面会我们回到FrameLayout的onMeasure()方法,当递归地执行完所有子View的测量工作后,会调用resolveSizeAndState()方法来根据之前的测量结果确定最终对FrameLayout的测量结果并存储起来。View类的resolveSizeAndState()方法的源码如下:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
  final int specMode = MeasureSpec.getMode(measureSpec);
  final int specSize = MeasureSpec.getSize(measureSpec);
  final int result;
  switch (specMode) {
    case MeasureSpec.AT_MOST:
      if (specSize < size) {
        // 父View给定的最大尺寸小于完全显示内容所需尺寸
        // 则在测量结果上加上MEASURED_STATE_TOO_SMALL
        result = specSize | MEASURED_STATE_TOO_SMALL;
      } else {
       result = size;
      }
      break;

    case MeasureSpec.EXACTLY:
      // 若specMode为EXACTLY,则不考虑size,result直接赋值为specSize
      result = specSize;
      break;

    case MeasureSpec.UNSPECIFIED:
    default:
      result = size;
  }

  return result | (childMeasuredState & MEASURED_STATE_MASK);

}

对于普通View,会调用View类的onMeasure()方法来进行实际的测量工作,该方法的源码如下:

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

对于普通View(非ViewgGroup)来说,只需完成自身的测量工作即可。以上代码中通过setMeasuredDimension()方法设置测量的结果,具体来说是以getDefaultSize()方法的返回值来作为测量结果。getDefaultSize()方法的源码如下:

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;
}

由以上代码我们可以看到,View的getDefaultSize()方法对于AT_MOST和EXACTLY这两种情况都返回了SpecSize作为result。所以若我们的自定义View直接继承了View类,我们就要自己对wrap_content (对应了AT_MOST)这种情况进行处理,否则对自定义View指定wrap_content就和match_parent效果一样了。

measure原理总结

通过上面分析可以看出measure过程主要就是从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程。具体measure核心主要有如下几点:

MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:

MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定; 
  • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。
  • 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。
  • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。
  • 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。
  • View的布局大小由父View和子View共同决定。
  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

layout

子View具体layout的位置都是相对于父容器而言的,View的layout过程和measure同理,也是从顶级View开始,递归的完成整个空间树的布局操作。

performLayout源码,会执行mView的layout方法:

//ViewRootImpl
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {

        final View host = mView;
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

    }

layout方法:

//ViewGroup

    //尽管ViewGroup也重写了layout方法
    //但是本质上还是会通过super.layout()调用View的layout()方法
    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {

            //如果无动画,或者动画未运行
            super.layout(l, t, r, b);
        } else {
            //等待动画完成时再调用requestLayout()
            mLayoutCalledWhileSuppressed = true;
        }
    }

//View

    public void layout(int l, int t, int r, int b) {
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        //如果布局有变化,通过setFrame重新布局
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

            //如果这是一个ViewGroup,还会遍历子View的layout()方法
            //如果是普通View,通知具体实现类布局变更通知
            onLayout(changed, l, t, r, b);

            //清除PFLAG_LAYOUT_REQUIRED标记
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            ``````
            //布局监听通知
        }

        //清除PFLAG_FORCE_LAYOUT标记
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    }

ViewGroup的onLayout()方法竟然是一个抽象方法,这就是说所有ViewGroup的子类都必须重写这个方法。所以在自定义ViewGroup控件中,onLayout配合onMeasure方法一起使用可以实现自定义View的复杂布局。自定义View首先调用onMeasure进行测量,然后调用onLayout方法动态获取子View和子View的测量大小,然后进行layout布局。重载onLayout的目的就是安排其children在父View的具体位置,重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。

layout阶段的大致流程我们就分析完了,这个阶段主要就是根据上一阶段得到的View的测量宽高来确定View的最终显示位置。显然,经过了measure阶段和layout阶段,我们已经确定好了View的大小和位置,那么接下来就可以开始绘制View了。

layout原理总结

整个layout过程比较容易理解,从上面分析可以看出layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。具体layout核心主要有以下几点:

View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。(结合自定义viewgrounp)

measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

draw

draw过程也是在ViewRootImpl的performTraversals()内部调运的,其调用顺序在measure()和layout()之后,这里的mView对于Actiity来说就是PhoneWindow.DecorView,ViewRootImpl中的代码会创建一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工。所以又回归到了ViewGroup与View的树状递归draw过程。

View.draw()方法源码:

public void draw(Canvas canvas) {
  . . . 
  // 绘制背景,只有dirtyOpaque为false时才进行绘制,下同
  int saveCount;
  if (!dirtyOpaque) {
    drawBackground(canvas);
  }
  . . . 
  // 绘制自身内容
  if (!dirtyOpaque) onDraw(canvas);
  // 绘制子View
  dispatchDraw(canvas);
   . . .
  // 绘制滚动条等
  onDrawForeground(canvas);

}

第一步,对View的背景进行绘制。
可以看见,draw方法通过调运drawBackground(canvas);方法实现了背景绘制。我们来看下这个方法源码,如下:

      private void drawBackground(Canvas canvas) {
        //获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable
        final Drawable background = mBackground;
        ......
        //根据layout过程确定的View位置来设置背景的绘制区域
        if (mBackgroundSizeChanged) {
            background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
            mBackgroundSizeChanged = false;
            rebuildOutline();
        }
        ......
            //调用Drawable的draw()方法来完成背景的绘制工作
            background.draw(canvas);
        ......
    }

第二步,对View的内容进行绘制。

可以看到,这里去调用了一下View的onDraw()方法,所以我们看下View的onDraw方法(ViewGroup也没有重写该方法),如下:

    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }

这是一个空方法。因为每个View的内容部分是各不相同的,所以需要由子类去实现具体逻辑。

第四步,对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制。
我们来看下View的draw方法中的dispatchDraw(canvas);方法源码,可以看见如下:

    protected void dispatchDraw(Canvas canvas) {}

View的dispatchDraw()方法是一个空方法,而且注释说明了如果View包含子类需要重写他,所以我们有必要看下ViewGroup的dispatchDraw方法源码(这也就是刚刚说的对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制的原因,因为如果是View调运该方法是空的,而ViewGroup才有实现),如下:

        @Override
    protected void dispatchDraw(Canvas canvas) {
        ......
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        ......
        for (int i = 0; i < childrenCount; i++) {
            ......
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            ......
            for (int i = disappearingCount; i >= 0; i--) {
                ......
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
    }

可以看见,ViewGroup确实重写了View的dispatchDraw()方法,该方法内部会遍历每个子View,然后调用drawChild()方法,我们可以看下ViewGroup的drawChild方法,如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

可以看见drawChild()方法调运了子View的draw()方法。所以说ViewGroup类已经为我们重写了dispatchDraw()的功能实现,我们一般不需要重写该方法,但可以重载父类函数实现具体的功能。

第六步,对View的滚动条进行绘制。

可以看到,这里去调用了一下View的onDrawScrollBars()方法,所以我们看下View的onDrawScrollBars(canvas);方法,如下:

    protected final void onDrawScrollBars(Canvas canvas) {
        //绘制ScrollBars分析不是我们这篇的重点,所以暂时不做分析
        ......
    }

可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。
到此,View的draw绘制部分源码分析完毕,我们接下来进行一些总结。

draw原理总结

可以看见,绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:

如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。

View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。

View的绘制是借助onDraw方法传入的Canvas类来进行的。

区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。

默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

invalidate and postInvalidate

我们知道invalidate()(在主线程)和postInvalidate()(可以在子线程)都是用于请求View重绘的方法,那么它是如何实现的呢?

invalidate方法源码分析:
来看一下View类中的一些invalidate方法(ViewGroup没有重写这些方法),如下:

  //public,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对局部View
    public void invalidate(Rect dirty) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        //实质还是调运invalidateInternal方法
        invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
                dirty.right - scrollX, dirty.bottom - scrollY, true, false);
    }
    public void invalidate(int l, int t, int r, int b) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        //实质还是调运invalidateInternal方法
        invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
    }
    public void invalidate() {
        //invalidate的实质还是调运invalidateInternal方法
        invalidate(true);
    }
    void invalidate(boolean invalidateCache) {
    //实质还是调运invalidateInternal方法
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

    //这是所有invalidate的终极调运方法!!!!!!
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        ......
            // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                //设置刷新区域
                damage.set(l, t, r, b);
                //传递调运Parent ViewGroup的invalidateChild方法
                p.invalidateChild(this, damage);
            }
            ......
    }

View的invalidate(invalidateInternal)方法实质是将要刷新区域直接传递给了父ViewGroup的invalidateChild方法,在invalidate中,调用父View的invalidateChild,这是一个从当前向上级父View回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集 。所以我们看下ViewGroup的invalidateChild方法,源码如下:

    public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;
        final AttachInfo attachInfo = mAttachInfo;
        ......
        do {
            ......
            //循环层层上级调运,直到ViewRootImpl会返回null
            parent = parent.invalidateChildInParent(location, dirty);
            ......
        } while (parent != null);
    }

这个过程最后传递到ViewRootImpl的invalidateChildInParent方法结束,所以我们看下ViewRootImpl的invalidateChildInParent方法,如下:

 @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        ......
        //View调运invalidate最终层层上传到ViewRootImpl后最终触发了该方法
        scheduleTraversals();
        ......
        return null

这个ViewRootImpl类的invalidateChildInParent方法直接返回了null,也就是上面ViewGroup中说的,层层上级传递到ViewRootImpl的invalidateChildInParent方法结束了那个do while循环。看见这里调运的scheduleTraversals这个方法吗?scheduleTraversals会通过Handler的Runnable发送一个异步消息,调运doTraversal方法,然后最终调用performTraversals()执行重绘。开头背景知识介绍说过的,performTraversals就是整个View数开始绘制的起始调运地方,所以说View调运invalidate方法的实质是层层上传到父级,直到传递到ViewRootImpl后触发了scheduleTraversals方法,然后整个View树开始重新按照上面分析的View绘制流程进行重绘任务。

postInvalidate方法

上面分析invalidate方法时注释中说该方法只能在UI Thread中执行,其他线程中需要使用postInvalidate方法,所以我们来分析分析postInvalidate这个方法源码。如下:

  public void postInvalidate() {
        postInvalidateDelayed(0);
    }

继续看下他的调运方法postInvalidateDelayed,如下:

    public void postInvalidateDelayed(long delayMilliseconds) {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        //核心,实质就是调运了ViewRootImpl.dispatchInvalidateDelayed方法
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

我们继续看他调运的ViewRootImpl类的dispatchInvalidateDelayed方法,如下源码:

public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

通过ViewRootImpl类的Handler发送了一条MSG_INVALIDATE消息,继续追踪这条消息的处理可以发现:

public void handleMessage(Message msg) {
    ......
    switch (msg.what) {
    case MSG_INVALIDATE:
        ((View) msg.obj).invalidate();
        break;
    ......
    }
    ......
}

handleMessage运行在主线程中,所以实质就是又在UI Thread中调运了View的invalidate();方法,那接下来View的invalidate();方法我们就不说了,上名已经分析过了。

View的requestLayout方法源码分析

和invalidate类似,其实在上面分析View绘制流程时或多或少都调运到了这个方法,而且这个方法对于View来说也比较重要,所以我们接下来分析一下他。如下View的requestLayout源码:

  public void requestLayout() {
        ......
        if (mParent != null && !mParent.isLayoutRequested()) {
            //由此向ViewParent请求布局
            //从这个View开始向上一直requestLayout,最终到达ViewRootImpl的requestLayout
            mParent.requestLayout();
        }
        ......
    }

当我们触发View的requestLayout时其实质就是层层向上传递,直到ViewRootImpl为止,然后触发ViewRootImpl的requestLayout方法,如下就是ViewRootImpl的requestLayout方法:

 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            //View调运requestLayout最终层层上传到ViewRootImpl后最终触发了该方法
            scheduleTraversals();
        }
    }

requestLayout()方法会改变mLayoutRequested 的值,在重绘过程中永爱判断只调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。

写在最后

在此声明:本文非原创,只是记录了自己的学习过程,下面链接是在学习中看的大神博客,大家可以看一下。

http://blog.csdn.net/xyh269/article/details/52904749
https://www.jianshu.com/p/060b5f68da79
http://blog.csdn.net/qinjuning/article/details/7110211/
https://www.jianshu.com/p/060b5f68da79
http://blog.csdn.net/qian520ao/article/details/78657084#t14
http://blog.csdn.net/yanbober/article/details/46128379/

猜你喜欢

转载自blog.csdn.net/say_from_wen/article/details/79086405