WindowInsets 分发
ViewRootImpl 在 performTraversals 时会调用 dispatchApplyInsets 方法
private void performTraversals() {// cache mView since it is used so much below...final View host = mView;...Rect frame = mWinFrame;if (mFirst) {...dispatchApplyInsets(host);} else {...}...
}
ViewRootImpl::dispatchApplyInsets 定义如下
public void dispatchApplyInsets(View host) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchApplyInsets");mApplyInsetsRequested = false;WindowInsets insets = getWindowInsets(true /* forceConstruct */);if (!shouldDispatchCutout()) {// Window is either not laid out in cutout or the status bar inset takes care of// clearing the cutout, so we don't need to dispatch the cutout to the hierarchy.insets = insets.consumeDisplayCutout();}host.dispatchApplyWindowInsets(insets);//1mAttachInfo.delayNotifyContentCaptureInsetsEvent(insets.getInsets(Type.all()));Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
注释 1 处的 host,其实就是在调用 ViewRootImpl::setView 时传递进来的 View 对象,通常来说,是一个 ViewGroup 对象
ViewGroup::dispatchApplyWindowInsets
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {insets = super.dispatchApplyWindowInsets(insets);//1if (insets.isConsumed()) {return insets;}if (View.sBrokenInsetsDispatch) {return brokenDispatchApplyWindowInsets(insets);} else {return newDispatchApplyWindowInsets(insets);}
}
注释 1 处先执行 View 的 dispatchApplyWindowInsets 方法
View::dispatchApplyWindowInsets
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {try {mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {//1return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);//2} else {return onApplyWindowInsets(insets);//3}} finally {mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;}
}
在注释 1 处判断,是否存在 mOnApplyWindowInsetsListener,该 Listener 可以通过 View:: setOnApplyWindowInsetsListener 来设置,如果设置过 mOnApplyWindowInsetsListener 则走注释 2 的代码,即调用 mOnApplyWindowInsetsListener 的 onApplyWindowInsets 方法;若未设置 mOnApplyWindowInsetsListener,则走注释 3 的代码,即继续执行 View::onApplyWindowInsets 的逻辑。
注意注释 2 和注释 3 是互斥的,如果调用 View::setOnApplyWindowInsetsListener 设置了 listener,则不会走注释 3 的逻辑。
先来看不设置 listener 的情况,即注释 2 处的代码
onApplyWindowInsets 分支
View::onApplyWindowInsets
public WindowInsets onApplyWindowInsets(WindowInsets insets) {if ((mPrivateFlags4 & PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS) != 0&& (mViewFlags & FITS_SYSTEM_WINDOWS) != 0) {return onApplyFrameworkOptionalFitSystemWindows(insets);//1}if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {// We weren't called from within a direct call to fitSystemWindows,// call into it as a fallback in case we're in a class that overrides it// and has logic to perform.if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {return insets.consumeSystemWindowInsets();//2}} else {// We were called from within a direct call to fitSystemWindows.if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {return insets.consumeSystemWindowInsets();//3}}return insets;
}
View::onApplyWindowInsets 根据两种情况来讨论
分支 1
注释 1 所在分支是第一种情况,DecorView 满足 (mPrivateFlags4 & PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS) != 0,参考下面代码
PhoneWindow::installDecor
private void installDecor() {if (mDecor == null) {mDecor = generateDecor(-1);mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);mDecor.setIsRootNamespace(true);if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);}} else {mDecor.setWindow(this);}if (mContentParent == null) {mContentParent = generateLayout(mDecor);// Set up decor part of UI to ignore fitsSystemWindows if appropriate.mDecor.makeFrameworkOptionalFitsSystemWindows();...}
}
View::makeFrameworkOptionalFitsSystemWindows
public void makeFrameworkOptionalFitsSystemWindows() {mPrivateFlags4 |= PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS;
}
(mViewFlags & FITS_SYSTEM_WINDOWS) != 0 可以通过View::setFitsSystemWindows 控制
View::setFitsSystemWindows
public void setFitsSystemWindows(boolean fitSystemWindows) {setFlags(fitSystemWindows ? FITS_SYSTEM_WINDOWS : 0, FITS_SYSTEM_WINDOWS);
}
Android S 上 DecorView 默认不具备 FITS_SYSTEM_WINDOWS Flag
分支 2
分支 1 不满足以后,第二个 if 语句用于判断是使用 fitSystemWindows 还是使用 fitSystemWindowsInt
View
protected boolean fitSystemWindows(Rect insets) {if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {if (insets == null) {// Null insets by definition have already been consumed.// This call cannot apply insets since there are none to apply,// so return false.return false;}// If we're not in the process of dispatching the newer apply insets call,// that means we're not in the compatibility path. Dispatch into the newer// apply insets path and take things from there.try {mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS;return dispatchApplyWindowInsets(new WindowInsets(insets)).isConsumed();} finally {mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS;}} else {// We're being called from the newer apply insets path.// Perform the standard fallback behavior.return fitSystemWindowsInt(insets);}
}private boolean fitSystemWindowsInt(Rect insets) {if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {//1Rect localInsets = sThreadLocal.get();boolean res = computeFitSystemWindows(insets, localInsets);//2applyInsets(localInsets);//3return res;}return false;
}
fitSystemWindows 是老方法,在 SDK 20 以后就已经不推荐使用了,主要研究 fitSystemWindowsInt
在注释 1 处,判断当前 View 是否设置了 android:fitsSystemWindows=”true” 如果设置了,则在注释 2 处计算 Insets,并在注释 3 处应用该 Insets
View::computeFitSystemWindows
protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) {WindowInsets innerInsets = computeSystemWindowInsets(new WindowInsets(inoutInsets),outLocalInsets);//1inoutInsets.set(innerInsets.getSystemWindowInsetsAsRect());return innerInsets.isSystemWindowInsetsConsumed();
}
computeFitSystemWindows 调用 computeSystemWindowInsets 计算 insets
public WindowInsets computeSystemWindowInsets(WindowInsets in, Rect outLocalInsets) {boolean isOptionalFitSystemWindows = (mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) != 0|| (mPrivateFlags4 & PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS) != 0;//1if (isOptionalFitSystemWindows && mAttachInfo != null) {OnContentApplyWindowInsetsListener listener =mAttachInfo.mContentOnApplyWindowInsetsListener;//2if (listener == null) {// The application wants to take care of fitting system window for// the content.outLocalInsets.setEmpty();return in;}Pair<Insets, WindowInsets> result = listener.onContentApplyWindowInsets(this, in);//3outLocalInsets.set(result.first.toRect());return result.second;} else {outLocalInsets.set(in.getSystemWindowInsetsAsRect());return in.consumeSystemWindowInsets().inset(outLocalInsets);}
}
注释 1 的判断是针对 DecorView 的,DecorView 带有 PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS
注释 2 的 mContentOnApplyWindowInsetsListener,是 PhoneWindow 根据mDecorFitsSystemWindows 调用过判断而给 ViewRootImpl 设置的监听器,用户可以通过调用 PhoneWindow::setDecorFitsSystemWindows 手动设置该值。
PhoneWindow
@Override
public void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) {mDecorFitsSystemWindows = decorFitsSystemWindows;applyDecorFitsSystemWindows();
}private void applyDecorFitsSystemWindows() {ViewRootImpl impl = getViewRootImplOrNull();if (impl != null) {impl.setOnContentApplyWindowInsetsListener(mDecorFitsSystemWindows? sDefaultContentInsetsApplier: null);}
}
注释 3 在 mContentOnApplyWindowInsetsListener 不为 null 的前提下,根据 ContentOnApplyWindowInsetsListener::onContentApplyWindowInsets 计算 insets
对于 DecorView 来说,这里调用的是下面的方法
/*** @see Window#setDecorFitsSystemWindows*/
private static final OnContentApplyWindowInsetsListener sDefaultContentInsetsApplier =(view, insets) -> {if ((view.getWindowSystemUiVisibility() & SYSTEM_UI_LAYOUT_FLAGS) != 0) {return new Pair<>(Insets.NONE, insets);}Insets insetsToApply = insets.getSystemWindowInsets();return new Pair<>(insetsToApply,insets.inset(insetsToApply).consumeSystemWindowInsets());};
对于 computeSystemWindowInsets 方法来说,它的 outLocalInsets 参数指应用的 insets 矩形,返回值用于标记该 insets 是否被消费过,如果 in.consumeSystemWindowInsets() 则说明该 insets 被消费了
回到 computeFitSystemWindows 方法
protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) {WindowInsets innerInsets = computeSystemWindowInsets(new WindowInsets(inoutInsets),outLocalInsets);//1inoutInsets.set(innerInsets.getSystemWindowInsetsAsRect());return innerInsets.isSystemWindowInsetsConsumed();//2
}
在注释 2 处根据当前 insets 是否被消费过,返回 true/false
View::fitSystemWindowsInt
private boolean fitSystemWindowsInt(Rect insets) {if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {//1Rect localInsets = sThreadLocal.get();boolean res = computeFitSystemWindows(insets, localInsets);//2applyInsets(localInsets);//3return res;}return false;
}
回到 View::fitSystemWindowsInt 的注释 3,在这里将应用 Insets
View::applyInsets
private void applyInsets(Rect insets) {mUserPaddingStart = UNDEFINED_PADDING;mUserPaddingEnd = UNDEFINED_PADDING;mUserPaddingLeftInitial = insets.left;mUserPaddingRightInitial = insets.right;internalSetPadding(insets.left, insets.top, insets.right, insets.bottom);
}
在这里将通过 internalSetPadding 将 insets 以 padding 的形式给 View 设置上
后续,回到 View::onApplyWindowInsets
public WindowInsets onApplyWindowInsets(WindowInsets insets) {if ((mPrivateFlags4 & PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS) != 0&& (mViewFlags & FITS_SYSTEM_WINDOWS) != 0) {return onApplyFrameworkOptionalFitSystemWindows(insets);//1}if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {// We weren't called from within a direct call to fitSystemWindows,// call into it as a fallback in case we're in a class that overrides it// and has logic to perform.if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {return insets.consumeSystemWindowInsets();//2}} else {// We were called from within a direct call to fitSystemWindows.if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {return insets.consumeSystemWindowInsets();//3}}return insets;
}
若 View::fitSystemWindowsInt 的注释 2 返回 true (即被消耗),则也会让 View::onApplyWindowInsets 的参数也变为被消耗的 Insets (注释 2、注释 3)
回到 ViewGroup::dispatchApplyWindowInsets
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {insets = super.dispatchApplyWindowInsets(insets);//1if (insets.isConsumed()) {//2return insets;}if (View.sBrokenInsetsDispatch) {//3return brokenDispatchApplyWindowInsets(insets);} else {return newDispatchApplyWindowInsets(insets);//4 }
}
若 insets 在注释 2 处判断为被消耗,则不会进行分发 WindowInsets,否则会继续执行注释 3 的逻辑判断是否中断分发, sBrokenInsetsDispatch 为 true 的条件如下
sBrokenInsetsDispatch = targetSdkVersion < Build.VERSION_CODES.R;
在 Android S 系统中,该值为 false,所以继续看注释 4 处的逻辑
ViewGroup::newDispatchApplyWindowInsets
private WindowInsets newDispatchApplyWindowInsets(WindowInsets insets) {final int count = getChildCount();for (int i = 0; i < count; i++) {getChildAt(i).dispatchApplyWindowInsets(insets);//1}return insets;
}
在注释 1 处,开始调用子 View 的 dispatchApplyWindowInsets 方法,让 WindowInsets 继续向子分发
对于低于 R 版本的系统来说,替代 newDispatchApplyWindowInsets 的是 brokenDispatchApplyWindowInsets
ViewGroup::brokenDispatchApplyWindowInsets
private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) {final int count = getChildCount();for (int i = 0; i < count; i++) {insets = getChildAt(i).dispatchApplyWindowInsets(insets);if (insets.isConsumed()) {break;}}return insets;
}
如果 app targetSdkVersion < 30 ,如果某个节点消费了 Insets,所有没遍历到的节点都不会收到 WindowInsets 的分发;
当 app 运行在 Android 11 以上版本的设备上且 targetSdkVersion >= 30,如果某个节点消费了 Insets,该节点的所有子节点不会收到 WindowInsets 分发。
区别在于一个节点消费了 WindowInsets 分发,其兄弟节点似乎否还能收到分发。
WindowInsets 相关类
InsetsSource
表示为单个类型 insets 的具体信息,例如 Insets 大小、是否可见等信息
InsetsState
InsetsState 持有当前系统 insets 的状态信息,其内部存有一个 InsetsSource 类型的数组 mSources
android.view.InsetsState#mSources
private final InsetsSource[] mSources = new InsetsSource[SIZE];
该数组用于保存当前屏幕中所有类型的 insets
当一个 Window 在客户端被添加、更新的时候,ViewRootImpl 将会调用 relayoutWindow 方法,从 WMS 获取当前 Window 的 InsetsState 信息
android.view.ViewRootImpl#relayoutWindow
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,boolean insetsPending) throws RemoteException {float appScale = mAttachInfo.mApplicationScale;boolean restore = false;if (params != null && mTranslator != null) {restore = true;params.backup();mTranslator.translateWindowLayout(params);}if (params != null) {if (DBG) Log.d(mTag, "WindowLayout in layoutWindow:" + params);if (mOrigWindowType != params.type) {// For compatibility with old apps, don't crash here.if (mTargetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {Slog.w(mTag, "Window type can not be changed after "+ "the window is added; ignoring change of " + mView);params.type = mOrigWindowType;}}}long frameNumber = -1;if (mSurface.isValid()) {frameNumber = mSurface.getNextFrameNumber();}int relayoutResult = mWindowSession.relayout(mWindow, params,(int) (mView.getMeasuredWidth() * appScale + 0.5f),(int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,mTempControls, mSurfaceSize);//1mPendingBackDropFrame.set(mTmpFrames.backdropFrame);if (mSurfaceControl.isValid()) {if (!useBLAST()) {mSurface.copyFrom(mSurfaceControl);} else {final Surface blastSurface = getOrCreateBLASTSurface();// If blastSurface == null that means it hasn't changed since the last time we// called. In this situation, avoid calling transferFrom as we would then// inc the generation ID and cause EGL resources to be recreated.if (blastSurface != null) {mSurface.transferFrom(blastSurface);}}if (mAttachInfo.mThreadedRenderer != null) {if (HardwareRenderer.isWebViewOverlaysEnabled()) {addPrepareSurfaceControlForWebviewCallback();addASurfaceTransactionCallback();}mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl);}} else {destroySurface();}mPendingAlwaysConsumeSystemBars =(relayoutResult & WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0;if (restore) {params.restore();}if (mTranslator != null) {mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame);mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);}setFrame(mTmpFrames.frame);//2mWillMove = false;mWillResize = false;mInsetsController.onStateChanged(mTempInsets);//3mInsetsController.onControlsChanged(mTempControls);//4return relayoutResult;
}
在注释 1 处,调用 IWindowSession::relayout 方法,IWindowSession 是 ViewRootImpl 与 WindowMangerService 的代理,从注释 1 处得到该当前 Window 的框架大小 mTmpFrames、以及 Insets 情况(mTempInsets、mTempControls),并在注释 2 、注释 3 、注释 4 将这些属性同步给 ViewRootImpl
注释 3、4 中的 mInsetsController 是 InsetsController 类型的实例,它实现了 WindowInsetsController 接口
WindowInsetsController
WindowInsetsController 是客户端用于控制 Window 的管理器,它在客户端的实现类是 InsetsController,每个 Window 会有一个 ViewRootImpl,每个 ViewRootImpl 都将持有一个 InsetsController 实例,客户端程序可以通过 View::getWindowInsetsController 得到该实例,对当前 Window 的 insets 进行控制
android.view.View#getWindowInsetsController
/*** Retrieves the single {@link WindowInsetsController} of the window this view is attached to.** @return The {@link WindowInsetsController} or {@code null} if the view is neither attached to* a window nor a view tree with a decor.* @see Window#getInsetsController()*/
public @Nullable WindowInsetsController getWindowInsetsController() {if (mAttachInfo != null) {return mAttachInfo.mViewRootImpl.getInsetsController();}ViewParent parent = getParent();if (parent instanceof View) {return ((View) parent).getWindowInsetsController();} else if (parent instanceof ViewRootImpl) {// Between WindowManager.addView() and the first traversal AttachInfo isn't set yet.return ((ViewRootImpl) parent).getInsetsController();}return null;
}
InsetsSourceControl
用于控制单独 InsetsSource 的控制器,被 InsetsController 所持有,同样也是在 ViewRootImpl:: relayoutWindow 时从 WMS 中获得
InsetsSourceConsumer
对单一 InsetsSource 的消费者,其内部持有 InsetsSourceControl,可以控制其leash的可见性和动画。
InsetsSourceControl 持有 InsetsSourceConsumer 的创造器,会在 InsetsController::getSourceConsumer 时创建实例
android.view.InsetsController#getSourceConsumer
public InsetsController(Host host) {this(host, (controller, type) -> {if (type == ITYPE_IME) {return new ImeInsetsSourceConsumer(controller.mState, Transaction::new, controller);} else {return new InsetsSourceConsumer(type, controller.mState, Transaction::new,controller);}}, host.getHandler());//1
}@VisibleForTesting
public @NonNull InsetsSourceConsumer getSourceConsumer(@InternalInsetsType int type) {InsetsSourceConsumer controller = mSourceConsumers.get(type);if (controller != null) {return controller;}controller = mConsumerCreator.apply(this, type);//2mSourceConsumers.put(type, controller);return controller;
}
注释 2 处实际调用的是注释 1 的箭头函数
InsetsStateController
负责在服务端管理全局 insets 状态信息,InsetsStateController 中的 mState 代表当前屏幕的所以类型的 Insets 状态信息
InsetsStateController 还持有不同类型的 InsetsSourceProvider,通常 DisplayContent、InsetsPolicy 等模块会通过 InsetsStateController 访问某一类型的 InsetsSourceProvider 实例
InsetsSourceProvider
负责在服务端根据 WindowState 配置、生成特定类型的 InsetsSource
com.android.server.wm.InsetsSourceProvider#getSource
/*** Updates the window that currently backs this source.** @param win The window that links to this source.* @param frameProvider Based on display frame state and the window, calculates the resulting* frame that should be reported to clients.* @param imeFrameProvider Based on display frame state and the window, calculates the resulting* frame that should be reported to IME.*/
void setWindow(@Nullable WindowState win,@Nullable TriConsumer<DisplayFrames, WindowState, Rect> frameProvider,@Nullable TriConsumer<DisplayFrames, WindowState, Rect> imeFrameProvider) {if (mWin != null) {if (mControllable) {mWin.setControllableInsetProvider(null);}// The window may be animating such that we can hand out the leash to the control// target. Revoke the leash by cancelling the animation to correct the state.// TODO: Ideally, we should wait for the animation to finish so previous window can// animate-out as new one animates-in.mWin.cancelAnimation();mWin.mProvidedInsetsSources.remove(mSource.getType());}ProtoLog.d(WM_DEBUG_IME, "InsetsSource setWin %s", win);mWin = win;mFrameProvider = frameProvider;mImeFrameProvider = imeFrameProvider;if (win == null) {setServerVisible(false);mSource.setFrame(new Rect());mSource.setVisibleFrame(null);} else {mWin.mProvidedInsetsSources.put(mSource.getType(), mSource);if (mControllable) {mWin.setControllableInsetProvider(this);if (mPendingControlTarget != null) {updateControlForTarget(mPendingControlTarget, true /* force */);mPendingControlTarget = null;}}}
}
InsetsSource getSource() {return mSource;
}
InsetsPolicy
被 DisplayContent 创建并持有,InsetsPolicy 负责管理不同类型 Inests 的 InsetsControlTarget 实例,同时 InsetsPolicy 还持有 InsetsStateController 类型实例,也提供了一些 Inests 相关行为策略
DisplayContent 对应一块屏幕的内容,一块屏幕对应一个 DisplayContent,一个 DisplayContent 下持有且仅一个 InsetsStateController 实例与 InsetsPolicy 实例,
InsetsControlTarget
InsetsControlTarget 是一个接口,代表可以控制 Insets 状态的对象,WindowState 实现了该接口,由于 WindowState 是每个 Window 在 WMS 中的状态描述,所以每个 Window 都具有控制 Insets 的能力,不同类型的 Insets 可能被不同的 Window 控制,可以通过 dump InsetsStateController 的信息查看当前 Insets 被谁控制
com.android.server.wm.InsetsStateController#dump
void dump(String prefix, PrintWriter pw) {pw.println(prefix + "WindowInsetsStateController");prefix = prefix + " ";mState.dump(prefix, pw);pw.println(prefix + "Control map:");for (int i = mTypeControlTargetMap.size() - 1; i >= 0; i--) {pw.print(prefix + " ");pw.println(InsetsState.typeToString(mTypeControlTargetMap.keyAt(i)) + " -> "+ mTypeControlTargetMap.valueAt(i));}pw.println(prefix + "InsetsSourceProviders:");for (int i = mProviders.size() - 1; i >= 0; i--) {mProviders.valueAt(i).dump(pw, prefix + " ");}
}