View的绘制流程

1.整体流程

View的首次绘制流程从ViewRootImpl的setView()方法开始

//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的绘制流程正式开始。
    }

ViewRootImpl在其创建过程中通过requestLayout()向主线程发送了一条触发遍历操作的消息,遍历操作是指performTraversals()方法。它是一个包罗万象的方法。

ViewRootImpl中接收的各种变化,如来自WMS的窗口属性变化、来自控件树的尺寸变化及重绘请求等都引发performTraversals()的调用,并在其中完成处理。

View类及其子类中的onMeasure()、onLayout()、onDraw()等回调也都是在performTraversals()的执行过程中直接或间接的引发。也正是如此,一次次的performTraversals()调用驱动着控件树有条不紊的工作,一旦此方法无法正常执行,整个控件树都将处于僵死状态。因此performTraversals()函数可以说是ViewRootImpl的心跳。 

//ViewRootImpl

    private void performTraversals() {

            //调用performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);

            performLayout(lp, mWidth, mHeight);

            performDraw();
    }

image

首先我们看一下performTraversals()首次绘制的大致流程,performTraversals()会依次调用performMeasure、performLayout、performDraw三个方法,这三个方法便是View绘制流程的精髓所在。

performMeasure : 会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传到子元素中了,这样就完成了一次measure过程。measure完成以后,可以通过getMeasuredWidth和getMeasureHeight方法来获取到View测量后的宽高。

performLayout : 和performMeasure同理。Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成以后,可以通过getTop/Bottom/Left/Right拿到View的四个顶点位置,并可以通过getWidth和getHeight方法来拿到View的最终宽高。

performDraw : 和performMeasure同理,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的。Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。

因此可以得出View的绘制分为measure,layout, draw 三个阶段:

  1. measure测量 :根据父 view 传递的 MeasureSpec 进行计算大小。
  2. layout布局:根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。
  3. draw绘制 :把 View 对象绘制到屏幕上。

2.measure测量

View的测量的目的便是 测量控件的宽高值,围绕该目的,View的设计者通过代码编织了一整套复杂的逻辑:

1、对于子View而言,其本身宽高直接受限于父View的 布局要求,举例来说,父View被限制宽度为40px,子View的最大宽度同样也需受限于这个数值。因此,在测量子View之时,子View必须已知父View的布局要求,这个 布局要求,  Android中通过使用 MeasureSpec 类来进行描述。

2、对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件本身未测量完毕,父控件自身的测量亦无从谈起。

Android中View的测量流程中使用了非常经典的 递归思想:对于一个完整的界面而言,每个页面都映射了一个View树,其最顶端的父控件测量开始时,会通过 遍历 将其 布局要求 传递给子控件,以开始子控件的测量,子控件在测量过程中也会通过 遍历 将其 布局要求 传递给它自己的子控件,如此往复一直到最底层的控件。

而当最底层位置的子控件自身测量完毕后,其父控件会将所有子控件的宽高数据进行聚合,然后通过对应的 测量策略 计算出父控件本身的宽高,测量完毕后,父控件的父控件也会根据其所有子控件的测量结果对自身进行测量,最终完成最顶层父控件的测量,至此界面整个View树测量完毕。

在整个 测量流程 中, 布局要求 都是一个非常重要的核心名词,Android中通过使用 MeasureSpec 类来对其进行描述。

子控件的测量过程本身还应该依赖于父控件的一些布局约束,子控件的测量结果是由父控件和其本身共同决定的 ,而父控件对子控件的布局约束,便是前文提到的 布局要求,即MeasureSpec类。

MeasureSpec

public final class MeasureSpec {

  int size;     // 测量大小
  Mode mode;    // 测量模式

  enum Mode { UNSPECIFIED, EXACTLY, AT_MOST }

  MeasureSpec(Mode mode, int size){
    this.mode = Mode;
    this.size = size;
  }

  public int getSize() { return size; }

  public Mode getMode() { return mode; }
}

MeasureSpec分成了2个属性。

测量大小 意味着控件需要对应大小的宽高;

测量模式 则表示控件对应的宽高模式: 

  • UNSPECIFIED:父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小;日常开发中自定义View不考虑这种模式,可暂时先忽略;
  •  EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;这里我们理解为控件的宽或者高被设置为 match_parent 或者指定大小,比如20dp;
  • AT_MOST:子元素至多达到指定大小的值;这里我们理解为控件的宽或者高被设置为wrap_content。

巧妙的是,Android并非通过上述定义MeasureSpec对象的方式对 布局要求 进行描述,而是使用了更简单的二进制的方式,用一个32位的int值进行替代:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30; //移位位数为30
    //int类型占32位,向右移位30位,该属性表示掩码值,用来与size和mode进行"&"运算,获取对应值。
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    //00左移30位,其值为00 + (30位0)
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //01左移30位,其值为01 + (30位0)
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //10左移30位,其值为10 + (30位0)
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    // 根据size和mode,创建一个测量要求
    public static int makeMeasureSpec(int size, int mode) {
        return size + mode;
    }

    // 根据规格提取出mode,
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    // 根据规格提取出size
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

这个int值中,前2位代表了测量模式,后30位则表示了测量的大小,对于模式和大小值的获取,只需要通过位运算即可。
以宽度举例来说,若我们设置宽度=5px(二进制对应了101),那么mode对应EXACTLY,在创建测量要求的时候,只需要通过二进制的相加,便可得到存储了相关信息的int值: 

图片

而当需要获得Mode的时候只需要用measureSpec与MODE_TASK相与即可,如下图:

图片

同理,想获得size的话只需要只需要measureSpec与~MODE_TASK相与即可,如下图:

图片

现在读者对MeasureSpec类有了初步地认识,在Android绘制过程中,View宽或者高的 布局要求 实际上是通过32位的int值进行的描述, 而MeasureSpec类本身只是一个静态方法的容器而已。

对于普通View(DecorView略有不同),其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽高。

那么针对不同父容器和View本身不同的LayoutParams,View就可以有多种MeasureSpec,我们从子View的角度来分析。

①View宽/高采用 固定宽高 的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY并且其大小遵循LayoutParams中的大小。

②View宽/高采用 wrap_content 的时候 ,不管父容器的模式是EXACTLY还是AT_MOST,View的模式总是AT_MOST,并且大小不能超过父容器的剩余空间(SpecSize,可用大小)。

③View宽/高采用 match_parent 的时候 : 

  • 如果父容器的模式是EXACTLY,那么View也是EXACTLY并且其大小是父容器的剩余空间(SpecSize,最终大小); 
  • 如果父容器的模式是AT_MOST,那么View也是AT_MOST并且其大小不会超过父容器的剩余空间(SpecSize,可用大小)。

å¾ç

我们知道View的测量是从ViewGroup开始递归测量所有的子View,测量完毕之后再测量自己。

而每个View自身的测量过程都包含3个重要的函数,分别为:

  • final void measure(int widthMeasureSpec, int heightMeasureSpec):执行测量的函数;
  • void onMeasure(int widthMeasureSpec, int heightMeasureSpec):真正执行测量的函数,开发者需要自己实现自定义的测量逻辑;
  • final void setMeasuredDimension(int measuredWidth, int measuredHeight):完成测量的函数,保存测量宽高;

同时包含子View的ViewGroup和View的测量过程也有些许不同;

1.measure()入口函数:标记测量的开始

首先父控件需要通过调用子控件的measure()函数,并同时将宽和高的 布局要求 作为参数传入,标志子控件本身测量的开始:

// 这个是父控件的代码,让子控件开始测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

对于View的测量流程,其必然包含了2部分:公共逻辑部分 和 开发者自定义测量的逻辑部分,为了保证公共逻辑部分代码的安全性,设计者将measure()方法配置了final修饰符:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  // ... 公共逻辑

  // 开发者需要自己重写onMeasure函数,以自定义测量逻辑
  onMeasure(widthMeasureSpec, heightMeasureSpec);
}

开发者不能重写measure()函数,并将View自定义测量的策略通过定义一个新的onMeasure()接口暴露出来供开发者重写。 

//View

    //final类,子类不能重写该方法
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

            ``````

            onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

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

    }

view.measure()方法其实没有实现任何测量的算法,它的作用在于判断是否需要引发onMeasure()的调用,并对onMeasure()行为的正确性进行检查。 

2.onMeasure:用来自定义View的测量策略

不同ViewGroup实现类有不同的测量方式,例如LinearLayout自身的高度是子View高度的累加。

在FrameLayout的onMeasure()方法中,会通过measureChildWithMargins()方法遍历子View,并且如果FrameLayout宽高的MeasureSpec是AT_MOST,那么FrameLayout计算自身宽高就会受到子View的影响,可能使用最大子View的宽高。DecorView是整个View树的根View,DecorView继承自FrameLayout,绘制流程也是从DecorView开始的。

//FrameLayout

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);

            //遍历子View,只要子View不是GONE便处理
            if (mMeasureAllChildren || child.getVisibility() != GONE) {

                //子View结合父View的MeasureSpec和自己的LayoutParams算出子View自己的MeasureSpec
                //如果当前child也是ViewGroup,也继续遍历它的子View
                //如果当前child是View,便根据这个MeasureSpec测量自己
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
               ``````
            }
        }

          ``````
        //父View等所有的子View测量结束之后,再来测量自己
        setMeasuredDimension(``````);

    }

measureChildWithMargins()方法为ViewGroup提供的方法,根据父View的MeasureSpec和子View的LayoutParams,算出子View自己的MeasureSpec。

//ViewGroup

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

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

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }


public static int getChildMeasureSpec(int spec, int padding, int childDimesion) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    // padding是指父容器中已占用的空间大小,因此子元素可用的
    // 大小为父容器的尺寸减去padding
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (sepcMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimesion == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us 
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size....
                // find out how big it should be
                resultSize = 0;
                resultMode == MeasureSpec.UNSPECIFIED;
            }
            break;
        }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。显然子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的margin和padding有关。

回顾一下上面的measure段落,因为具体实现的ViewGroup会重写onMeasure(),因为ViewGroup是一个抽象类,其测量过程的onMeasure()方法需要具体实现类去实现,这么做的原因在于不同的ViewGroup实现类有不同的布局特性,比如说FrameLayout,RelativeLayout,LinearLayout都有不同的布局特性,因此ViewGroup无法对onMeasure()做同一处理。

View自身也提供了onMeasure()函数默认的测量策略:

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

 以宽度为例,通过这样获取View默认的宽度:

getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)

1.在某些情况下(比如自身设置了minWidth或者background属性),View需要通过getSuggestedMinimumWidth()函数作为默认的宽度值:

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

2.这之后,将所得结果作为参数传递到getDefaultSize(minWidth, widthMeasureSpec)函数中,根据 布局要求 计算出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;
      // match_parent、wrap_content则返回布局要求中的size值
      case MeasureSpec.AT_MOST:
      case MeasureSpec.EXACTLY:
          result = specSize;
          break;
    }
    return result;
}

从getDefaultSize()方法的实现来看,对于AT_MOST和EXACTLY这两种情况View的宽高都由specSize决定,也就是说如果我们直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身的大小,否则在布局中使用wrap_content就相当于使用match_parent。 

3.setMeasuredDimension():保存测量数据

setMeasuredDimension(width,height)函数的存在意义非常重要,在onMeasure()执行自定义测量策略的过程中,调用该函数标志着View的测量得出了结果:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 普遍意义上,setMeasuredDimension()标志着测量结束
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
 // measuredWidth 测量结果,View的宽度
 // measuredHeight 测量结果,View的高度
 // 省略其它代码...

 // 该方法的本质就是将测量结果存起来,以便后续的layout和draw流程中获取控件的宽高
 mMeasuredWidth = measuredWidth;
 mMeasuredHeight = measuredHeight;
}

该函数被设计为由protected final修饰,这意味着只能由子类进行调用而不能重写。函数调用完毕,开发者可以通过getMeasuredWidth()或者getMeasuredHeight()来获取View测量的宽高,代码设计大概是这样: 

public final int getMeasuredWidth() {
    return mMeasuredWidth;
}
public final int getMeasuredHeight() {
    return mMeasuredHeight;
}

// 如何使用
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight()

经过measure() -> onMeasure() -> setMeasuredDimension()函数的调用,最终View自身测量流程执行完毕。

3.layout布局

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

经过前面的测量,控件树中的控件对于自己的尺寸显然已经了然于胸。而且父控件对于子控件的位置也有了眉目,所以经过测量过程后,布局阶段会把测量结果转化为控件的实际位置与尺寸。控件的实际位置与尺寸由View的mLeft,mTop,mRight,mBottom 等4个成员变量存储的坐标值来表示。

并且需要注意的是: View的mLeft,mTop,mRight,mBottom 这些坐标值是以父控件左上角为坐标原点进行计算的。倘若需要获取控件在窗口坐标系中的位置可以使用View.getLocationInWindow()或者是View.getRawX()/Y()。

//ViewRootImpl


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

        final View host = mView;

        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

    }
//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也重写了layout()方法,但是本质上还是会走View的layout()。

在View的layout()方法里,首先通过 setFrame()(setOpticalFrame()也走setFrame())将l、t、r、b分别设置到mLeft、mTop、mRight、和mBottom,这样就可以确定子View在父容器的位置了,上面也说过了,这些位置是相对父容器的。

然后调用onLayout()方法,使具体实现类接收到布局变更通知。如果此类是ViewGroup,还会遍历子View的layout()方法使其更新布局。

//View

    protected void onLayout(boolean changed, int l, int t, int r, int b) {}



//ViewGroup

    //abstract修饰,具体实现类必须重写该方法
    @Override
    protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

对于普通View来说,onLayout()方法是一个空实现,主要是具体实现类重写该方法后能够接收到布局坐标更新信息。

对于ViewGroup来说,和measure一样,不同实现类有它不同的布局特性,在ViewGroup中onLayout()方法是abstract的,具体实现类必须重写该方法,以便接收布局坐标更新信息后,处理自己的子View的坐标信息。

//FrameLayout.java
    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

小结
对比测量measure和布局layout两个过程有助于加深对它们的理解。

  • measure确定的是控件的尺寸,并在一定程度上确定了子控件的位置。而布局则是针对测量结果来实施,并最终确定子控件的位置。
  • measure结果对布局过程没有约束力。虽说子控件在onMeasure()方法中计算出了自己应有的尺寸,但是由于layout()方法是由父控件调用,因此控件的位置尺寸的最终决定权掌握在父控件手中,测量结果仅仅只是一个参考。
  • 因为measure过程是后根遍历(DecorView最后setMeasureDiemension()),所以子控件的测量结果影响父控件的测量结果。
  • 而Layout过程是先根遍历(layout()一开始就调用setFrame()完成DecorView的布局),所以父控件的布局结果会影响子控件的布局结果。

4.draw绘制

View的draw过程相对上面measure和layout比较简单。

View的draw过程遵循如下几步 :

绘制背景drawBackground();

绘制自己onDraw();

如果是ViewGroup则绘制子View,dispatchDraw();

绘制装饰(滚动条)和前景,onDrawForeground();

//View

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;

        //检查是否是"实心(不透明)"控件。(后面有补充)
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;
        //非"实心"控件,将会绘制背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

        //如果控件不需要绘制渐变边界,则可以进入简便绘制流程
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);//非"实心",则绘制控件本身

            // Step 4, draw the children
            dispatchDraw(canvas);//如果当前不是ViewGroup,此方法则是空实现

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);//绘制装饰和前景

            // we're done...
            return;
        }

        ``````
    }

5.核心方法

requestLayout、invalidate与postInvalidate

//View

    public void invalidate() {
        invalidate(true);
    }

    void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {

        //如果VIew不可见,或者在动画中
        if (skipInvalidate()) {
            return;
        }

        //根据mPrivateFlags来标记是否重绘
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            if (fullInvalidate) {//上面传入为true,表示需要全部重绘
                mLastIsOpaque = isOpaque();//
                mPrivateFlags &= ~PFLAG_DRAWN;//去除绘制完毕标记。
            }

            //添加脏区标记。
            mPrivateFlags |= PFLAG_DIRTY;

            //清除缓存,表示由当前View发起的重绘。
            if (invalidateCache) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }

            //把需要重绘的区域传递给父View
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                //设置重绘区域(区域为当前View在父容器中的整个布局)
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }
            ``````
        }
    }

上述代码中,会设置一系列的标记位到mPrivateFlags中,并且通过父容器的invalidateChild方法,将需要重绘的脏区域传给父容器。(ViewGroup和ViewRootImpl都继承了ViewParent类,该类中定义了子元素与父容器间的调用规范。)

//ViewGroup

    @Override
    public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;

        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {

                RectF boundingRect = attachInfo.mTmpTransformRect;
                boundingRect.set(dirty);

                ``````  
               //父容器根据自身对子View的脏区域进行调整

                transformMatrix.mapRect(boundingRect);
                dirty.set((int) Math.floor(boundingRect.left),
                        (int) Math.floor(boundingRect.top),
                        (int) Math.ceil(boundingRect.right),
                        (int) Math.ceil(boundingRect.bottom));

            // 这里的do while方法,不断的去调用父类的invalidateChildInParent方法来传递重绘请求
            //直到调用到ViewRootImpl的invalidateChildInParent(责任链模式)
            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }

                if (drawAnimation) {
                    if (view != null) {
                        view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                    } else if (parent instanceof ViewRootImpl) {
                        ((ViewRootImpl) parent).mIsAnimating = true;
                    }
                }

                //如果父类是"实心"的,那么设置它的mPrivateFlags标识
                // If the parent is dirty opaque or not dirty, mark it dirty with the opaque
                // flag coming from the child that initiated the invalidate
                if (view != null) {
                    if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
                            view.getSolidColor() == 0) {
                        opaqueFlag = PFLAG_DIRTY;
                    }
                    if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
                        view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
                    }
                }

                //***往上递归调用父类的invalidateChildInParent***
                parent = parent.invalidateChildInParent(location, dirty);

                //设置父类的脏区域
                //父容器会把子View的脏区域转化为父容器中的坐标区域
                if (view != null) {
                    // Account for transform on current parent
                    Matrix m = view.getMatrix();
                    if (!m.isIdentity()) {
                        RectF boundingRect = attachInfo.mTmpTransformRect;
                        boundingRect.set(dirty);
                        m.mapRect(boundingRect);
                        dirty.set((int) Math.floor(boundingRect.left),
                                (int) Math.floor(boundingRect.top),
                                (int) Math.ceil(boundingRect.right),
                                (int) Math.ceil(boundingRect.bottom));
                    }
                }
            } 
            while (parent != null);
        }
    }

由于最上层的ViewParent是ViewRootImpl,所以我们可以查看ViewRootImpl的invalidateChildInParent方法即可。

//ViewRootImpl

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        //检查线程,这也是为什么invalidate一定要在主线程的原因
        checkThread();

        if (dirty == null) {
            invalidate();//有可能需要绘制整个窗口
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }

        ``````

        invalidateRectOnScreen(dirty);

        return null;
    }


    //设置mDirty并执行View的工作流程
    private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
            mAttachInfo.mSetIgnoreDirtyState = true;
            mAttachInfo.mIgnoreDirtyState = true;
        }

        // Add the new dirty rect to the current one
        localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);    
        //在这里,mDirty的区域就变为方法中的dirty,即要重绘的脏区域

        ``````
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();//执行View的工作流程
        }
    }

invalidate()方法也调用到了scheduleTraversals()!
因此也会执行View的绘制流程perforMeasure()、performLayout()、perforDraw()。

这个scheduleTraversals()很眼熟,在setView()->requestLayout()中见过,并且我们还说了mLayoutRequested用来表示是否measure和layout。

//ViewRootImpl

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


    private void performTraversals() {

        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
        if (layoutRequested) {
            measureHierarchy(```);//measure
        }


        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);//layout
        }


        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
        if (!cancelDraw && !newSurface) {
            performDraw();//draw
        }
    } 

因为我们invalidate的时候,并没有设置mLayoutRequested,所以它只走performDraw()流程,并且在draw()流程中会清除mDirty区域。

postInvalidate

首先看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;
        if (attachInfo != null) {
                                 attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

会调用到ViewRootImpl的dispatchInvalidateDelayed方法,就是用handler在非主线程发送了一个异步消息

 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是在主线程调用,postInvalidate在非主线程中调用,这就是它们的去别。 

requestLayout

在view.measure()的方法里,仅当给与的MeasureSpec发生变化时,或要求强制重新布局时,才会进行测量。

强制重新布局 : 控件树中的一个子控件内容发生变化时,需要重新测量和布局的情况,在这种情况下,这个子控件的父控件(以及父控件的父控件)所提供的MeasureSpec必定与上次测量时的值相同,因而导致从ViewRootImpl到这个控件的路径上,父控件的measure()方法无法得到执行,进而导致子控件无法重新测量其布局和尺寸。(在父容器measure中遍历子元素)

解决途径 : 因此,当子控件因内容发生变化时,从子控件到父控件回溯到ViewRootImpl,并依次调用父控件的requestLayout()方法。这个方法会在mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT,从而使得这些父控件的measure()方法得以顺利执行,进而这个子控件有机会进行重新布局与测量。这便是强制重新布局的意义所在。

//View

    @CallSuper
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        ``````

        // 增加PFLAG_FORCE_LAYOUT标记,在measure时会校验此属性
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        // 父类不为空&&父类没有请求重新布局(是否有PFLAG_FORCE_LAYOUT标志)
        //这样同一个父容器的多个子View同时调用requestLayout()就不会增加开销
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }

    }

最顶层的ViewParent是ViewRootImpl。

//ViewRootImpl

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

同样,requestLayout()方法会调用scheduleTraversals(),因为设置了mLayoutRequested =true标识,所以在performTraversals()中调用performMeasure(),performLayout(),但是由于没有设置mDirty,所以不会走performDraw()流程。

但是,requestLayout()方法就一定不会导致onDraw()的调用吗?

在上面layout()方法中说道 :

在View的layout()方法里,首先通过 setFrame()(setOpticalFrame()也走setFrame())将l、t、r、b分别设置到mLeft、mTop、mRight、和mBottom,这样就可以确定子View在父容器的位置了,上面也说过了,这些位置是相对父容器的。

//View -->  layout()

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;


        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            //布局坐标改变了
            changed = true;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);//调用invalidate重新绘制视图


            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            ``````
        }
        return changed;
    }

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值