关闭

View的源码分析(绘制流程以及刷新机制)

574人阅读 评论(0) 收藏 举报
分类:

一、基本知识

    1:ViewRoot

    ViewRoot是连接WindowManager与DecorView的纽带,View的整个绘制流程的三大步(measure、layout、draw)都是通过ViewRoot完成的。当Activity对象被创建完毕后,会将

DecorView添加到Window中(Window是对窗口的抽象,DecorView是一个窗口的顶级容器View,其本质是一个FrameLayout),同时会创建ViewRootImpl(ViewRoot的实现类)

对象,并将ViewRootImpl与DecorView建立关联。关于ViewRoot,我们只需要知道它是联系GUI管理系统和GUI呈现系统的纽带。View的绘制流程从ViewRoot的performTraversals

方法开始,经过measure、layout、draw三大过程完成对一个View的绘制工作。peformTraversal方法内部会调用measure、layout、draw这三个方法,这三个方法内部又分别调用

onMeasure、onLayout、onDraw方法。

   2:MeasureSpec

     MeasureSpec为一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,前者指测量模式,后者指某种测量模式下的规格大小。在一个View的measure过程中,系统会

将该View的LayoutParams结合父容器的“要求”生成一个MeasureSpec,这个MeasureSpec说明了应该怎样测量这个View。

 (1)三种 SpecMode:
     UNSPECIFIED:父容器不对View作任何要求,通常用于系统内部,表示一种测量的状态。

  EXACTLY:父容器已经检测出View所需要的精确大小,这种测量模式下View的测量值就是SpecSize的值。这个SpecMode对应于LayoutParams中的match_parent和给出具体大小

这两种模式。

AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于此值,可用大小取决于不同View的具体实现。这个SpecMode对应于LayoutParams中的wrap_content。

(2)对于DecorView,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同确定;对于普通View,他的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共

同确定。

二、View的绘制流程及源码分析

    整个View树的绘图流程是在ViewRootImpl类的performTraversals()方法开始的,该函数做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小(measure)、是否

重新放置视图的位置(layout)、以及是否重绘 (draw),其核心也就是通过判断来选择顺序执行这三个方法中的哪个,如下:

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.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
        ......
        mView.draw(canvas);
        ......
    }
<pre name="code" class="java"> private int getRootMeasureSpec(int windowSize, int rootDimension) {  
     int measureSpec;  
     switch (rootDimension) {  
         case ViewGroup.LayoutParams.MATCH_PARENT:  
             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
              break;  
          case ViewGroup.LayoutParams.WRAP_CONTENT:  
             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
              break;  
         default:  
             measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
             break;  
     }  
    return measureSpec;  
 }


    1:Measure源码分析

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        //回调onMeasure()方法
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
    }
   (1):普通View的Measure过程

普通View的measure方法是由ViewGroup在measureChild方法中调用的(即完成了measure过程从ViewGroup到子View的传递),ViewGroup调用其子View的measure时即传入了

该子View的widthMeasureSpec和heightMeasureSpec。注意到measure是一个final方法,因此要实现自定义的measure过程,需要重写onMeasure方法,onMeasure方法源码如下

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
             getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
 }
 public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //通过MeasureSpec解析获取mode与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;
    }
正常情况下(SpecMode为AT_MOST或EXACTLY),getDefaultSize获取的尺寸大小即为specSize。由以上代码还可知道,直接继承View的自定义控件需要重写onMeasure方法

并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent的效果。

(2):ViewGroup的绘制过程

ViewGroup中的onMeasure()方法主要是遍历子View,先测量所有子View的之后设置自身的大小值

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     if (mOriention == VERTICAL) {
         measureVertical(widthMeasureSpec, heightMeasureSpec);
     } else {
         measureHorizontal(widthMeasureSpec, heightMeasureSpec);
     }
 }
以MeasureVertical为例:

 for (int i = 0; i < count; ++i) {
      final View child = getVirtualChildAt(i);
      . . .
      //Determine how big this child would like to be. If this or previous children have given a weight, 

       //then we allow it to use all available space (and we will shrink things later if needed).
     measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, totalHeight == 0 ? mTotalLength : 0);
      
      if (oldHeight != Integer.MIN_VALUE) {
          lp.height = oldHeight;
     }
 
     final int childLength = child.getMeasuredHeight();
     final int totalLength = mTotalLength;
     mTotalLength = Math.max(totalLength, totalLength+childHeight+lp.topMargin+lp.bottomMargin+getNextLocationOffset(child));
 }
在measureChildBeforeLayout方法内部会调用子元素的measure方法,这样会依次让每个子元素进入measure过程。mTotalLength表示LinearLayout在竖直方向上的尺寸,每完成

一个子元素的measure过程,它的值也会相应增加。测量完子元素后,LinearLayout会测量自身的大小。

    对于一般的ViewGroup主要通过其measureChildren方法完成其子View的measure过程,上面垂直LinearLayout中调用的measureChildBeforeLayout可以看做是measureChildren的一个“变种”,measureChildren方法代码如下:

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];  
              if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { 
                  measureChild(child, widthMeasureSpec, heightMeasureSpec);  
              }  
      }  
 }
通过measureChild()完成对子View的Measure();
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);  
 }  

注意在这里,在执行child.measure方法前,就已经通过getChildMeasureSpec获取了子View的MeasureSpec。getChildMeasureSpec根据子View的LayoutParams和父容器的

MeasureSpec来决定子View的MeasureSpec,getChildMeasureSpec的具体作用是:

a. 当childLayoutParams指定为为具体的大小时:若parentSpecMode为EXACTLY,则childSpecMode为EXACTLY,childSpecSize为childSize(layout_width和layout_height中指

定的具体大小);若parentSpecMode为AT_MOST,则childSpecMode和childSpecSize分别为EXACTLY和childSize。

b. 当childLayoutParams为match_parent时:若parentSpecMode为EXACTLY,则childSpecMode和childSpecSize分别为EXACTLY和parentSize(父容器中可用的大小);若

parentSpecMode为AT_MOST,则childSpecMode和childSpecSize分别为AT_MOST和parentSize。

c. 当childLayoutParams为wrap_content时:若parentSpecMode为EXACTLY,则childSpecMode和childSpecSize分别为AT_MOST和parentSize;若parentSpecMode为

AT_MOST,则childSpecMode和childSpecSize分别为AT_MOST和parentSize。

2:layout()源码分析

    layout过程用来确定View在父容器中的位置,因而是由父容器获取子View的位置参数后,调用child.layout方法并传入已获取的位置参数,从而完成对子View的layout。当

ViewGroup的位置被确定后,它在onLayout中会遍历所有子元素并调用其layout方法,在layout方法中子元素的onLayout又会被调用。layout方法确定先View本身的位置,再调用

onLayout方法确定所有子元素的位置。

    以LinearLayout为例:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
     if (mOriention == VERTIVAL) {
         layoutVertical(l, t, r, b);
     } else {
         layoutHorizontal(l, t, r, b);
     }
 }

void layoutVertical(int left, int top, int right, int bottom) {
      . . .
      final int count = getVirtualChildCount();
      for (int i = 0; i < count; i++) {
          final View child = getVirtualChildAt(i);
          if (child == null) {
              childTop += measureNullChild(i);
          } else if (child.getVisibility() != GONE) {
              final int childWidth = child.getMeasuredWidth();
             final int childHeight = child.getMeasuredHeight();
             
             final int LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
             . . .
             if (hasDividerBeforeChildAt(i)) {
                 childTop += mDividerHeight;
             }
  
             childTop += lp.topMargin;
             setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
             childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
             
             i += getChildrenSkipCount(child, i);
         }
     }
 }
通过 setChildFrame设置子View的位置。

3:draw()源码分析

   draw()的过程主要有以下几部分

  

 public void draw(Canvas canvas) {
        // Step 1, draw the background, if needed
        ......
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        ......

        // Step 2, save the canvas' layers
        ......
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
        ......

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children,对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制。
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        ......
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ......

        // Step 6, draw decorations (scrollbars)  对view的滚动条进行绘制
        onDrawScrollBars(canvas);
        ......
    }
三、View的刷新机制的源码分析

    1:invalidate()

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);
    }
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方法,如下:

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绘制流程进行重绘任务。

2:postInvalidate源码分析  

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

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

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

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


常见的引起invalidate方法操作的原因主要有:

直接调用invalidate方法.请求重新draw,但只会绘制调用者本身。

触发setSelection方法。请求重新draw,但只会绘制调用者本身。

触发setVisibility方法。 当View可视状态在INVISIBLE转换VISIBLE时会间接调用invalidate方法,继而绘制该View。当View的可视状态在INVISIBLE\VISIBLE 转换为GONE状态时

会间接调用requestLayout和invalidate方法,同时由于View树大小发生了变化,所以会请求measure过程以及draw过程,同样只绘制需要“重新绘制”的视图。

触发setEnabled方法。请求重新draw,但不会重新绘制任何View包括该调用者本身。

触发requestFocus方法。请求View树的draw过程,只绘制“需要重绘”的View。

3:程序在第一次是通过什么绘制View的呢?

 @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        ......
        //如果mContentParent为空进行一些初始化,实质mContentParent是通过findViewById(ID_ANDROID_CONTENT);获取的id为content的FrameLayout的布局(不清楚的请先看《Android应用setContentView与LayoutInflater加载解析机制源码分析》文章)
        if (mContentParent == null) {
            installDecor();
        } 
        ......
        //把我们的view追加到mContentParent
        mContentParent.addView(view, params);
        ......
    }<pre name="code" class="java"> public void addView(View child, int index, LayoutParams params) {
        ......
        //该方法稍后后面会详细分析
        requestLayout();
        //重点关注!!!
        invalidate(true);
        ......
    }


对于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()方法会调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。


1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:5530次
    • 积分:215
    • 等级:
    • 排名:千里之外
    • 原创:16篇
    • 转载:3篇
    • 译文:0篇
    • 评论:2条
    文章分类
    文章存档
    最新评论