当前位置: 首页 > 工具软件 > lin-view-ui > 使用案例 >

Android GUI系统-ViewTree遍历过程(五)

伯向晨
2023-12-01

ViewTree的遍历过程performTraversals

遍历过程主要对应三个函数performMeasure(计算大小),perforLayout(计算位置),performDraw(绘制内容)。具体看下这三个步骤执行的条件,及怎么执行的?


1performMeasure

private void performTraversals() @ViewRootImpl.java{
//mView是ViewTree的根。
	final View host = mView;

// mWinFrame是WMS计算后的结果,通过mWindowSession.relayout(...mWinFrame...)传递回来的。
	Rect frame = mWinFrame;
//mFirst是不是第一次执行performTraversal。
	if (mFirst) {
		mFullRedrawNeeded = true;
		mLayoutRequested = true;
	}else{
		desiredWindowWidth = frame.width();
		desiredWindowHeight = frame.height();
//当前的宽高跟期望值不一致。
		if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
              	mFullRedrawNeeded = true;
              	mLayoutRequested = true;
              	windowSizeMayChange = true;
		}
	}
// measureHierarchy是第一次可能会调用performMeasure的地方。mStopped表示窗口的拥有者Activity处于stopped状态,所以这个窗口就不再是active的。
	boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
	if (layoutRequested) {
		windowSizeMayChange |= measureHierarchy(host, lp, res,
			desiredWindowWidth, desiredWindowHeight);
	}
}

//是否执行performMeasure,前面的layoutRequested是其中一部分条件,measureHierarchy方法中还有一些判断。


private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,final 	Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
	boolean goodMeasure = false;
//View请求的宽是 WRAP_CONTENT。
	if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
// mTmpValue是期望的对话框的最大宽度,
		res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
		if (baseSize != 0 && desiredWindowWidth > baseSize) {
			performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
		}
	}

//如果View请求的宽度不是 WRAP_CONTENT, goodMeasure为false,会执行 performMeasure。
	if (!goodMeasure) {
		childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
		childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
		performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
	}
}


如果上面的条件没能满足performMeasure的执行条件,performTraversals往下走,还有判断是不是执行performMeasure


private void performTraversals() @ViewRootImpl.java{
// mStopped当前activity不处于stoped状态; mReportNextDraw通常在点亮屏幕扥带第一次绘制时,用于加快systemUI跟窗口管理之间的交互时,设置为true。
	if (!mStopped || mReportNextDraw) {
//触摸模式是否引起焦点变化。
		boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
		(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
//WMS计算后得到的宽高跟View经过onMeasure计算的值不一致。
		if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
			|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
			updatedConfiguration) {
			performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
		}
	}
}

以上条件满足,开始执行performMeasure


private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
//直接调用ViewTree的根元素mView的 measure,View中measure和onMeasure子类都可以重载,通常建议重载onMeasure,测量工作也是onMeasure中进行的。
	mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}	

measure方法会调用onMeasure执行实际的测量工作。


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

其中的参数widthMeasureSpecheightMeasureSpec是上一级父View要求的宽高,依次往下传递到viewTree的各个元素。在ViewRootImpl中,这两个值是通过getRootMeasureSpec计算得出的,然后传给ViewTree的根元素DecorView

子类在重写onMeasure方法时,必须调用setMeasuredDimension来存储该view测量后的宽高。


插播下widthMeasureSpecheightMeasureSpec这两个值的解释,这两个值是通过 

childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);

计算出来的,MeasureSpec是View的子类,也即是说widthMeasureSpec这个int值中包含了两部分,一是size,一是mode(MeasureSpec.EXACTLY),详细看下它的格式:

View.java

public static class MeasureSpec {

        private static final int MODE_SHIFT = 30;

        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

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

}

IntRange 表表示size应该在一个范围内,重点看(size & ~MODE_MASK) | (mode & MODE_MASK);

MODE_MASK 是0x3左移30位,结果是0x11000等等后面都是0,所以size & ~MODE_MASK即是把size的最高两位空出来了,而mode & MODE_MASK 等于只取mode的最高两位,最终的结果:(size & ~MODE_MASK) | (mode & MODE_MASK) 的最高两位是mode,剩余的位数表示size



看一个具体View的子类,如何重写onMeasure


protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) @FrameLayout.java{
	int count = getChildCount();
//循环处理所有的子View。
	for (int i = 0; i < count; i++) {
		final View child = getChildAt(i);
//测量每个子对象,条件是 mMeasureAllChildren为true,相关的属性是:android.R.styleable#FrameLayout_measureAllChildren;并且View是可见的。
		if ( mMeasureAllChildren || child.getVisibility() != GONE) {
//测量子对象,考虑padding,margins,如果子view是个ViewGroup,则会调用其child.measure函数继续递归,直到叶节点。
			measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
//得到view(Child)的计算结果,child.getMeasuredWidth()对应了View的MeasuredWidth变量,这个值通常是在onMeasure结束时通过setMeasuredDimension来设置的。
                	maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                	maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
		}
	}

//考虑其他因素,如padding,对于View类来说,它只有padding,padding指的是内容区域与外围边框的距离,有left、top、right、bottom四个值;对于ViewGroup来说,除了padding,还有margin,margin指的是内容中各个元素之间的距离。
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

//对前面测量的宽高的最大值做调整,
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

//保存测量的结果。
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
}

2performLayout计算位置。

private void performTraversals() @ViewRootImpl.java{
	final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
//这个条件跟前面performMeasure类似, mWidth, mHeight是由WMS计算后的值,表示WMS期望的宽高。
	if (didLayout) {
		performLayout(lp, mWidth, mHeight);
	}
}


private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) @ViewRootImpl.java{
	final View host = mView;
//调用的ViewTree根元素的方法 layout,传入的参数也是DecorView的测量后的宽高。
	host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}

子类不应该重载layout方法,而是应该重载onLayout方法,然后在onLayout方法中,调用每个子viewlayout方法。四个参数表示的是这个view相对于它的父view四个方向的位置。


public void layout(int l, int t, int r, int b) {
//通过setFrame将这4个边距记录到成员变量中mLeft;mTop;mBottom;mRight;
	boolean changed = isLayoutModeOptical(mParent) ?
		setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

//view.java中的onLayout是null的,调用具体子类的onLayout。
	if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == 			
		PFLAG_LAYOUT_REQUIRED) {
		onLayout(changed, l, t, r, b);
	}
//将flag恢复。
	mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
}

protected void onLayout(boolean changed, int left, int top, int right, int bottom) 
	@FrameLayout.java{
	layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) 
	@FrameLayout.java{
	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) {
//子view设置的layout属性。
			final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//在onMeasure中计算的宽高,这里在计算出left,top就可以确定一个子view在父View中具体位置。
			final int width = child.getMeasuredWidth();
			final int height = child.getMeasuredHeight();
			int childLeft;
			int childTop;
// gravity属性,是在不改变子View大小的情况下,控制其在父View中的位置,比如放在父对象的顶部、左部、垂直居中、水平居中等。
			int gravity = lp.gravity;
			final int layoutDirection = getLayoutDirection();
			final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
			final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
//根据具体的布局属性,计算left。
			switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    	case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
			}
//根据具体的布局属性,计算top。
			 switch (verticalGravity) {
                    		case Gravity.CENTER_VERTICAL:
                       	 childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        	 lp.topMargin - lp.bottomMargin;
                      	  break;
			}
//调用子View的layout方法,依次递归下去。
			child.layout(childLeft, childTop, childLeft + width, childTop + height);
		}
	}
}


3)performDraw。遍历流程的最后一步,

private void performTraversals() @ViewRootImpl.java{
	boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
// cancelDraw表示当前draw被取消,需要重新遍历; 在前面relayoutWindow过程中,如果WMS没有成功申请到surface,newSurface会置为true,这是也需要重新遍历。
	if (!cancelDraw && !newSurface) {
// performDraw重点调用了draw方法。
		performDraw();
	}
}

图形绘制的方式分软件和硬件两种,什么时候会使用硬件方式绘制呢?

当执行ActivityonCreate方法时,调用了PhoneWindow.javasetContentView,接着调用了PhoneWindow.javainstallDecor


private void installDecor() @PhoneWindow.java{
// mDecor为null时,调用 generateDecor传入的featureId是-1,这个id将会影响后期的绘制方式。
	if (mDecor == null) {
		mDecor = generateDecor(-1);
	}else{…...}
}

生成DecorView对象时,把这个featureId保存在DecorView的变量mFeatureId中。除了生成应用程序的DecorViewfeatureId-1,其他类型窗口的featureId都是大于0的值,如:


public abstract class Window @Window.java{
    /** Flag for the "options panel" feature.  This is enabled by default. */
    public static final int FEATURE_OPTIONS_PANEL = 0;
    /** Flag for the context menu.  This is enabled by default. */
    public static final int FEATURE_CONTEXT_MENU = 6;
    /**
     * Flag for enabling the Action Bar.
     * This is enabled by default for some devices. The Action Bar
     * replaces the title bar and provides an alternate location
     * for an on-screen menu button on some devices.
     */
    public static final int FEATURE_ACTION_BAR = 8;
…...	
}

只有featureid小于0时,willYouTakeTheSurface方法的返回值才可能不为null


public android.view.SurfaceHolder.Callback2 willYouTakeTheSurface() @DecorView.java{
	return mFeatureId < 0 ? mWindow.mTakeSurfaceCallback : null;
}

ViewRootImpl.java中,willYouTakeTheSurface方法的返回值,会影响到会不会生成对象mAttachInfo.mHardwareRenderermAttachInfo.mHardwareRenderer= ThreadedRenderer.create(mContext, translucent);)而这个对象判断绘制方式时会用到。


在回头看下performDraw调用的draw方法。


private void draw(boolean fullRedrawNeeded) @ViewRootImpl.java{
//surface是画板,如果没有合法的surface,UI数据就无法正常存储和显示。
	Surface surface = mSurface;
	if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
//这个条件下,调用硬件绘制的方式。
		if (mAttachInfo.mHardwareRenderer != null && 			
			mAttachInfo.mHardwareRenderer.isEnabled()) {
			mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
		}else{
//否则,调用软件绘制的方式。
			if (!drawSoftware(surface, mAttachInfo, 
				xOffset, yOffset, scalingRequired, dirty)) {
				return;
			}
		}
	}
}

看下软件渲染是怎么绘制的。


private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) @ViewRootImpl.java{
//canvas是作画的工具集。
	final Canvas canvas;
//锁定一个canvas对象,将使用这个canvas把数据画到surface中。
	canvas = mSurface.lockCanvas(dirty);

	mView.mPrivateFlags |= View.PFLAG_DRAWN;
//做必要的坐标变换。
	canvas.translate(-xoff, -yoff);
	mTranslator.translateCanvas(canvas);

//从Viewtree的根开始绘制。
	mView.draw(canvas);
//绘制结束,解锁canvas,提交结果,canvas的绘制结果通过surface提交给surfacefligner,最后合成到framebuffer中,才能显示到屏幕上。
	surface.unlockCanvasAndPost(canvas);
}


 类似资料: