View绘制流程—performTraversals的后续逻辑

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/user11223344abc/article/details/82774328

前言

我们知道View绘制流程是在performTraversals()这个方法内展开的(perdormDraw(),perdormMeasure(),perdormLayout()),之前我们分析过app是如何抵达performTraversals这个方法的(戳我)
那么此篇的目的是在performTraversals之后更为具体的逻辑。

铺垫

在展开具体的讨论之前,我们需要知道一些基础知识点。

  • 基础认知1
    1.在ActivityThread中,当Acitivty对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
    2.ViewRootImpl是连接WindowManager和DecorView的纽带。
    3.View三大流程均是藉由ViewRoot来完成的。

  • 基础认知2
    1.measure过程决定View的宽高,当这个过程执行完成,可以通过getMeasureWidth和getMeasureHeight来获取View测量后的宽高,至于获取的结果究竟和实际情况是否相符,这个是有特殊情况的,但大部分时间是相符的。
    2.Layout过程决定View的四个顶点坐标和实际View的宽高,完成之后可以通过getTop,getBottom,getLeft,getRight来拿到View四个顶点的位置。并能通过getWidth(),getHeaght()来获取最终实际的宽高。
    3.Draw过程决定View的显示,只有draw完成之后View内从才能显示到屏幕上。

  • 基础认知3
    1.DecorView是顶级View,它是一个FrameLayout。
    2.DecorView它内部包含一个竖直方向的LinearLayout。这个LinearLayout里有上下俩部分(android主题相关),上面是标题栏,下面是内容栏。
    3.在Activity中我们通过setContentView所设置的布局文件其实是被加到内容栏之中的。内容栏的系统id是R.id.content,这个内容栏和DecorView一样也是个FramentLayout
    4.获取内容栏:findViewById(R.android.id.content),获取内容栏中的View,content.getChildAt(0)

  • MeasureSpec基本认识
    1.MeasureSpec很大程度上决定了一个View的尺寸规格(View的尺寸规格还受父布局影响)
    2.MeasureSpec是一个32位的int值,高2位代表SpecMode—测量模式,低30位代表SpecSize—在某种测量模式下的规格大小
    3.MeasureSpec通过将specMode和specSize打包成一个int值来避免过多的对象内存分配
    4.MeasureSpec不仅仅是由LayoutParams来决定,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。但DecorView有点特殊,它的MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定。MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。
    5.对于我们普通布局中的View,它的Measure是由ViewGroup传递而来。受父控件的宽高影响,同时也受padding,margin影响。
    6.SpecMode有三种类型:
    a.unspecified
    父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
    b.exactly
    父容器已经检测出View所需要的精确大小,这个时候View最终代销就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这俩种模式。
    c.at_most
    父容器指定了一个可用大小即specSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于layoutParams中的wrap_content

View工作流程

measure确定View的测量宽高
layout决定View的四个顶点,以及最终宽高
draw将View绘制到屏幕

Measure过程

1.若执行的对象是View,则只需要measure自己。
2.若执行的对象是ViewGroup,measure了自己之后,还会去mesure自己的子元素。

View的measure过程

1.final View.java#measure() -> View.onMeasure // 由View的measure方法为入口,该方法为final类型,子类无法去重写实现。接着去调用View的onMeasure方法,

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

2.View.onMeasure() -> 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;
    }

a> case MeasureSpec.AT_MOST:和case MeasureSpec.EXACTLY:情况
getDefaultSize返回的大小result就是measureSpec中specSize,这个specSize就是View测量后的大小,虽然View的实际大小是在layout阶段确定下来的,但基本上大多数View的实际大小就等同于measure阶段的大小。
b> case MeasureSpec.UNSPECIFIED:情况
没做处理,直接将传进来的size参数当做结果result返回出去了,而传进来的参数又是由这俩个方法决定:getSuggestedMinimumWidth(),getSuggestedMinimumHeight()。

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

这2段代码的意思是,如果没设置背景,返回mMinWidth或者mMinHeight,这二者对应的值是android:minWidth,android:minHeight。不设置默认为0.
如果设置了背景就调用 max(mMinWidth, mBackground.getMinimumWidth()或者 max(mMinHeight, mBackground.getMinimumHeight()方法。什么意思?
mBackground.getMinimumHeight()的意思就是返回背景的最小原始高度,mBackground.getMinimumWidth()同理是获得背景的最小原始宽度,若没有就是0。max方法就是在二者之间去最大值。
总结下getSuggestedMinimumWidth(),getSuggestedMinimumHeight()就是。
若没有设置背景,返回android:minHeight,若设置了背景,取原始宽高和最小宽高的最大值。
4.分析了这么多,再回过头来看,View的meausure流程,通过measure方法调到onMeasure方法,在onMeasure方法内通过向setMeasuredDimension方法传参从而实现测量的过程。而传的这俩个参数是由方法getDefaultSize()根据MeasureSpec的三种模式进行计算的的,当是case MeasureSpec.AT_MOST:和case MeasureSpec.EXACTLY:模式时,就是测量measureSpec中specSize,当是case MeasureSpec.UNSPECIFIED:情况时,这种情况就是系统测量的情况,具体逻辑就是根据有无背景图来判断是返回原始尺寸。另外贴出setMeasuredDimension以下源码:

  protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

ViewGroup的measure过程

1.ViewGroup是一个抽象类且是View的子类,它并没复写onMeasure这个方法(它可以重写onMeasure但是并没有选择去重写这个方法。),为什么?因为各种ViewGroup的布局方式不尽相同,所以它要求这些不同的ViewGroup各自实现自己不同的onMeasure方法。
2.虽然它没有复写onMeasure这个方法,但它提供了一个measureChildren()方法。这个方法就是遍历子View,挨个measureChild()

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

3.measureChild的逻辑就是去除子View的LayoutParams和childMeasureSpec,再通过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);
    }

关于在Activity启动时去拿View的宽高

场景:当我们在一个已经启动的Activity的各个回调方法内去拿View宽高是拿不到的,可能会拿到的宽高是0。因为View的measure方法和activity的生命周期方法并不是同步执行的。因此无法保证Activity的onCreate,onResume,onStart时某个View已经测量完毕了。

作者给出了4种解决方案。
1.Activity/View#onWindowFocus
这个方法是View已经绘制完毕准备好了就会回调,但是注意它可能会频繁的调用多次。
2.View.post

View.post(new Runnable(){
	run(){
		...
    }
})

3.ViewTreeObserver
4.手动measure,分情况,有的情况下无法测量出来,比如match_parent时

Layout过程

1.View内有个空实现的onLayout方法。

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

2.ViewGroup内也只是抽象方法,没去实现。

protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

3.以View#layout()为起点,该方法内部会去调用,layout方法

  if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
	......
    onLayout(changed, l, t, r, b);
	......
  }

4.各个ViewGroup需要自己去实现自己的onLayout细节逻辑。
5.layout方法主要是处理子View如何布局

   child.layout(left, top, left + width, top + height);

一个常规的实现:

@Override      
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {      
	int mTotalHeight = 0;      
	// 当然,也是遍历子View,每个都要告诉ViewGroup      
	int childCount = getChildCount();      
	for (int i = 0; i < childCount; i++) {      
		View childView = getChildAt(i);      
		// 获取在onMeasure中计算的视图尺寸      
		int measureHeight = childView.getMeasuredHeight();      
		int measuredWidth = childView.getMeasuredWidth();      
		childView.layout(left, mTotalHeight, measuredWidth, mTotalHeight + measureHeight);          
		mTotalHeight += measureHeight;      
	}      
}  

Draw流程

	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;   
	        // Step 1, draw the background, if needed
	        int saveCount;
	        if (!dirtyOpaque) {
	            drawBackground(canvas);
	        }
		...
	   // Step 2, save the canvas' layers
	        int paddingLeft = mPaddingLeft;
	        final boolean offsetRequired = isPaddingOffsetRequired();
	        if (offsetRequired) {
	            paddingLeft += getLeftPaddingOffset();
	        }
		...
	  // Step 3, draw the content
	        if (!dirtyOpaque) onDraw(canvas);
	        // Step 4, draw the children
	        dispatchDraw(canvas);
		...
	   // Step 5, draw the fade effect and restore layers
	        final Paint p = scrollabilityCache.paint;
	        final Matrix matrix = scrollabilityCache.matrix;
	        final Shader fade = scrollabilityCache.shader;
		...
	  // Step 6, draw decorations (scrollbars)
	        onDrawScrollBars(canvas);
	        if (mOverlay != null && !mOverlay.isEmpty()) {
	            mOverlay.getOverlayView().dispatchDraw(canvas);
	        }
	   }

1.如果有设置背景,则绘制背景
2.保存canvas层
3.绘制自身内容
4.如果有子元素则绘制子元素
5.绘制效果
6.绘制装饰品(scrollbars)

总结

  • MeasureSpec
    1.MeasureSpec很大程度上决定了一个View的尺寸规格(View的尺寸规格还受父布局影响)
    2.MeasureSpec是一个32位的int值,高2位代表SpecMode—测量模式,低30位代表SpecSize—在某种测量模式下的规格大小
    3.MeasureSpec通过将specMode和specSize打包成一个int值来避免过多的对象内存分配
    4.MeasureSpec不仅仅是由LayoutParams来决定,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。但DecorView有点特殊,它的MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定。MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。
    5.对于我们普通布局中的View,它的Measure是由ViewGroup传递而来。受父控件的宽高影响,同时也受padding,margin影响。

  • SpecMode有三种类型:
    a.unspecified
    父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
    b.exactly
    父容器已经检测出View所需要的精确大小,这个时候View最终代销就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这俩种模式。
    c.at_most
    父容器指定了一个可用大小即specSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于layoutParams中的wrap_content

  • 源码调用链

	ActivityThread.java#handleResumeActivity()
	WindowManagerImpl.java#addView()
	ViewRoot#setView()
	ViewRoot#requestLayout()
	ViewRoot#scheduleTraversals()
	ViewRoot#handleMessage() - > final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
               doTraversal();
            }
        }
	ViewRootImpl()#performTraversals()
	ViewRootImpl#measureHierarchy(){
		performMeasure()
		performLayout()
		performDraw()
	}
  • measure主要职责测量View的宽高,分为View的measure和ViewGroup的measure,
    a>View的measure
    从View的measure方法调到onMeasure方法,onMeasure内通过setMeasureDimiision对View进行测量,setMeasureDimiision接收的参数根据情况分为2类:
    一类是case MeasureSpec.AT_MOST:case MeasureSpec.EXACTLY:
    这个是从MeasureSpec中解压出来的sepcSize
    另一类是unspecified,这个测量的是原始宽高。
    b>ViewGroup的measure
    它没有复写onMeasure,而是提供了一个measureChildren,主要是测量自己然后还要测量子View。

  • layout
    layout主要就是ViewGroup负责执行,一般是对子View进行布局摆放,确定4个坐标点,另外measure内的宽高并不是最终的宽高,到了layout方法内才是确定的最终宽高。

  • draw
    1.如果有设置背景,则绘制背景
    2.保存canvas层
    3.绘制自身内容
    4.如果有子元素则绘制子元素
    5.绘制效果
    6.绘制装饰品(scrollbars)

猜你喜欢

转载自blog.csdn.net/user11223344abc/article/details/82774328