WindowManagerService Inset(5)
简介
窗口中有一个Inset的概念,比较常见的主要是状态栏,导航栏,输入法。我们先来说说Inset的作用,前面我们已经了解了WMS树是怎么管理窗口信息的,我们平时在使用app时,点击输入框后弹出输入法,输入法窗口可以把我们app窗口往上抬,前面我们知道输入法窗口所在的节点是13,14层,而app窗口所在节点是第2层,并不是父子关系或者直接的兄弟节点。这种关联如果仅仅靠之前树的结构不太好实现,Inset就是用来实现处理这种场景的。
Inset在框架侧的主要数据结构
WMS中每个DisplayContent通过一个InsetsStateController来管理该屏幕上所有的Insets,而InsetsStateController又通过InsetsState来管理所有Insets,有两个InsetsState,一个是当前用于显示的,一个是在更改的(类似双缓冲)。
InsetsStateController还有一个InsetsSourceProvider,他持有一个InsetsSource。InsetsState中则记录的所有的InsetsSource。
InsetsSource表示一个Insets的元数据。而InsetsSourceProvider则包含了一些额外的计算逻辑以及一些动画处理。
Inset创建过程
Inset是在添加窗口时创建的,我们用原生的SystemUi里面NavigationBar来举例。
在通过addView添加窗口时,我们需要提供一个LayoutParams,而Inset就是通过设置LayoutParams的providedInsets实现的,我们来看看NavigationBar的这个变量的设置。
1.1 getBarLayoutParamsForRotation
private WindowManager.LayoutParams getBarLayoutParamsForRotation(int rotation) {
// ...
// 详见1.2
lp.providedInsets = getInsetsFrameProvider(insetsHeight, userContext);
// ...
}
1.2 getInsetsFrameProvider
该方法返回了一个InsetsFrameProvider数组,其中每一个InsetsFrameProvider都代表需要创建的一个Inset,可以看到这里会构建非常多个InsetsFrameProvider
其中比较重要的是每个InsetsFrameProvider都会通过setInsetsSize设置这个Inset的大小,注意这里上下左右并不是说这个Inset上下左右所在的位置,而是说这个窗口上下左右分别要预留出多少作为Inset,后面我们会看到它的计算方式。
这里我们就盯着navBarProvider来看吧,这里仅仅给他设置了一个bottom为insetsHeight,left,top,right都为0.
InsetsFrameProvider除了一个InsetsSize以外,还有一个通过setInsetsSizeOverrides来设置的InsetsSizeOverrides,这个是针对特定类型窗口计算Insets占位时,Insets有特定大小。
private InsetsFrameProvider[] getInsetsFrameProvider(int insetsHeight, Context userContext) {
// 构造navBarProvider
final InsetsFrameProvider navBarProvider =
new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.navigationBars())
.setInsetsSizeOverrides(new InsetsFrameProvider.InsetsSizeOverride[] {
new InsetsFrameProvider.InsetsSizeOverride(
TYPE_INPUT_METHOD, null)});
if (insetsHeight != -1 && !mEdgeBackGestureHandler.isButtonForcedVisible()) {
// 设置navBarProvider的InsetSize
navBarProvider.setInsetsSize(Insets.of(0, 0, 0, insetsHeight));
}
final boolean needsScrim = userContext.getResources().getBoolean(
com.android.internal.R.bool.config_navBarNeedsScrim);
navBarProvider.setFlags(needsScrim ? 0 : FLAG_SUPPRESS_SCRIM, FLAG_SUPPRESS_SCRIM);
// ...
return new InsetsFrameProvider[] {
navBarProvider,
tappableElementProvider,
mandatoryGestureProvider,
new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.systemGestures())
.setSource(InsetsFrameProvider.SOURCE_DISPLAY)
.setInsetsSize(Insets.of(gestureInsetsLeft, 0, 0, 0))
.setMinimalInsetsSizeInDisplayCutoutSafe(
Insets.of(gestureInsetsLeft, 0, 0, 0)),
new InsetsFrameProvider(mInsetsSourceOwner, 1, WindowInsets.Type.systemGestures())
.setSource(InsetsFrameProvider.SOURCE_DISPLAY)
.setInsetsSize(Insets.of(0, 0, gestureInsetsRight, 0))
.setMinimalInsetsSizeInDisplayCutoutSafe(
Insets.of(0, 0, gestureInsetsRight, 0))
};
}
1.3 WindowManagerService.addWindow
中间addView添加窗口过程上一章介绍过,这里直接看addWindow方法中和Inset相关部分的逻辑。
addWindowLw里面会添加Inset,updateAboveInsetsState会更新Insets,而这里参数false表示不要通知app侧Inset的变更,因为我们前面看过这里addWindow之一把WindowState添加的窗口树下,并没有做窗口的大小都测量,所以这里是没有准确的窗口大小的,那么Insets的计算依赖于窗口大小,我们一会会看到。
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, @InsetsType int requestedVisibleTypes,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,
float[] outSizeCompatScale) {
// ...详见1.4
displayPolicy.addWindowLw(win, attrs);
// ...
displayContent.getInsetsStateController().updateAboveInsetsState(
false /* notifyInsetsChanged */);
// ...
// 回传InsetsState给app
outInsetsState.set(win.getCompatInsetsState(), true /* copySources */);
// 构建InsetsSourceControl数组给app,它包含了Insets和回传的目标窗口关联的一些信息。
getInsetsSourceControls(win, outActiveControls);
}
1.4 DisplayPolicy.addWindowLw
void addWindowLw(WindowState win, WindowManager.LayoutParams attrs) {
switch (attrs.type) {
case TYPE_NOTIFICATION_SHADE:
mNotificationShade = win;
break;
case TYPE_STATUS_BAR:
mStatusBar = win;
break;
case TYPE_NAVIGATION_BAR:
mNavigationBar = win;
break;
}
// 前面看到这个providedInsets是app侧layoutParams带过来的,是InsetsFrameProvider数组,这里遍历这个数组,构建Insets
if (attrs.providedInsets != null) {
for (int i = attrs.providedInsets.length - 1; i >= 0; i--) {
final InsetsFrameProvider provider = attrs.providedInsets[i];
// 这里获取的是一个回调函数,详见1.4.1
final TriFunction<DisplayFrames, WindowContainer, Rect, Integer> frameProvider =
getFrameProvider(win, i, INSETS_OVERRIDE_INDEX_INVALID);
final InsetsFrameProvider.InsetsSizeOverride[] overrides =
provider.getInsetsSizeOverrides();
final SparseArray<TriFunction<DisplayFrames, WindowContainer, Rect, Integer>>
overrideProviders;
if (overrides != null) {
overrideProviders = new SparseArray<>();
for (int j = overrides.length - 1; j >= 0; j--) {
overrideProviders.put(
overrides[j].getWindowType(), getFrameProvider(win, i, j));
}
} else {
overrideProviders = null;
}
// 这里会构建一个Inset所关联的InsetSourceProvider,并调用setWindowContainer将InsetSourceProvider和WindowContainer相互关联。详见1.5
mDisplayContent.getInsetsStateController().getOrCreateSourceProvider(
provider.getId(), provider.getType()).setWindowContainer(
win, frameProvider, overrideProviders);
mInsetsSourceWindowsExceptIme.add(win);
}
}
}
1.4.1 getFrameProvider
这个方法会根据display包含的信息和窗口自身信息,以及Inset来计算出最终窗口大小信息。displayFrames主要包含一些和屏幕相关的信息,水平或者垂直,屏幕可见区域,可见区域剪去屏幕的cutout区域剩余区域。cutout是指例如刘海屏的刘海区域。
最终计算出Inset的实际区域存储在inOutFrame里。这个回调方法在窗口大小变化的时候会调用,因为Insets区域和窗口是有关联的,所以当窗口大小发生了变化,就需要重新计算Insets区域,而Insets变化后,可能又会引起其他窗口变化,所以WMS对窗口的测量的方法可能会重复执行,后续我们会专门介绍这个流程。
private static TriFunction<DisplayFrames, WindowContainer, Rect, Integer> getFrameProvider(
WindowState win, int index, int overrideIndex) {
return (displayFrames, windowContainer, inOutFrame) -> {
final LayoutParams lp = win.mAttrs.forRotation(displayFrames.mRotation);
final InsetsFrameProvider ifp = lp.providedInsets[index];
final Rect displayFrame = displayFrames.mUnrestricted;
final Rect safe = displayFrames.mDisplayCutoutSafe;
boolean extendByCutout = false;
// 根据type不同选择源大小是窗口大小还是屏幕大小。
switch (ifp.getSource()) {
case SOURCE_DISPLAY:
inOutFrame.set(displayFrame);
break;
case SOURCE_CONTAINER_BOUNDS:
inOutFrame.set(windowContainer.getBounds());
break;
case SOURCE_FRAME:
extendByCutout =
(lp.privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0;
break;
case SOURCE_ARBITRARY_RECTANGLE:
inOutFrame.set(ifp.getArbitraryRectangle());
break;
}
final Insets insetsSize = overrideIndex == INSETS_OVERRIDE_INDEX_INVALID
? ifp.getInsetsSize()
: ifp.getInsetsSizeOverrides()[overrideIndex].getInsetsSize();
if (ifp.getMinimalInsetsSizeInDisplayCutoutSafe() != null) {
sTmpRect2.set(inOutFrame);
}
// inOutFrame上面设置成源大小,insetsSize是Inset的大小。详见1.4.2
calculateInsetsFrame(inOutFrame, insetsSize);
// extendByCutout表示需要适配cutout区域。
if (extendByCutout && insetsSize != null) {
WindowLayout.extendFrameByCutout(safe, displayFrame, inOutFrame, sTmpRect);
}
if (ifp.getMinimalInsetsSizeInDisplayCutoutSafe() != null) {
calculateInsetsFrame(sTmpRect2, ifp.getMinimalInsetsSizeInDisplayCutoutSafe());
WindowLayout.extendFrameByCutout(safe, displayFrame, sTmpRect2, sTmpRect);
if (sTmpRect2.contains(inOutFrame)) {
inOutFrame.set(sTmpRect2);
}
}
return ifp.getFlags();
};
}
1.4.2 calculateInsetsFrame
计算逻辑很简单,比如依照我们前面navBar的Inset,只有bottom不为0,则将inOutFrame的top设置成inOutFrame.bottom - insetsSize.bottom,其余的不变,所以可以看到我们前面app侧传Inset的时候四个值都只有一个值不为0.
举个具体的数字例子:我们这里inOutFrame是屏幕大小,比如是(0,0,1000,2000),然后navBar传的bottom是200,结果就是(0,1800,1000,2000),相当于就是屏幕最底部高为200的区域作为导航键。
private static void calculateInsetsFrame(Rect inOutFrame, Insets insetsSize) {
if (insetsSize == null) {
return;
}
if (insetsSize.left != 0) {
inOutFrame.right = inOutFrame.left + insetsSize.left;
} else if (insetsSize.top != 0) {
inOutFrame.bottom = inOutFrame.top + insetsSize.top;
} else if (insetsSize.right != 0) {
inOutFrame.left = inOutFrame.right - insetsSize.right;
} else if (insetsSize.bottom != 0) {
inOutFrame.top = inOutFrame.bottom - insetsSize.bottom;
} else {
inOutFrame.setEmpty();
}
}
1.5 InsetsSourceProvider.setWindowContainer
将窗口和InsetsSourceProvider相互关联。这样窗口也持有InsetsSourceProvider,后续当窗口大小变化时,可以通过InsetsSourceProvider来重新计算窗口Insets。
void setWindowContainer(@Nullable WindowContainer windowContainer,
@Nullable TriFunction<DisplayFrames, WindowContainer, Rect, Integer> frameProvider,
@Nullable SparseArray<TriFunction<DisplayFrames, WindowContainer, Rect, Integer>>
overrideFrameProviders) {
if (mWindowContainer != null) {
if (mControllable) {
mWindowContainer.setControllableInsetProvider(null);
}
mWindowContainer.cancelAnimation();
mWindowContainer.getInsetsSourceProviders().remove(mSource.getId());
mSeamlessRotating = false;
}
// 记录windowContainer,frameProvider(1.4.2返回的一个方法)
mWindowContainer = windowContainer;
mFrameProvider = frameProvider;
mOverrideFrames.clear();
mOverrideFrameProviders = overrideFrameProviders;
if (windowContainer == null) {
setServerVisible(false);
mSource.setVisibleFrame(null);
mSource.setInsetsRoundedCornerFrame(false);
mSourceFrame.setEmpty();
} else {
// 这里的mSource就是InsetsSource,可以看出来InsetsSource和InsetsSourceProvider是一一对应的
mWindowContainer.getInsetsSourceProviders().put(mSource.getId(), this);
if (mControllable) {
mWindowContainer.setControllableInsetProvider(this);
if (mPendingControlTarget != null) {
updateControlForTarget(mPendingControlTarget, true /* force */);
mPendingControlTarget = null;
}
}
}
}
Inset变化后通知应用流程
2.1 RootWindowContianer.performSurfacePlacementNoTrace
这个方法是WMS的一个核心方法,WMS就是通过这个方法来对所有窗口进行测量计算的,我们后面会专门来将这个方法,这里只关注通知Inset变化的一条链路。
void performSurfacePlacementNoTrace() {
// ...
// 里面会调用DisplayContent的applySurfaceChangesTransaction,我们直接看这个,详见2.2
applySurfaceChangesTransaction();
// ...
}
2.2 DisplayContent.applySurfaceChangesTransaction
里面有调用InsetsStateController.onPostLayout
void applySurfaceChangesTransaction() {
// ...
// 详见2.3
mInsetsStateController.onPostLayout();
// ...
}
2.3 InsetsStateController.onPostLayout
调用没有InsetsSourceProvider都调用onPostLayout重新计算Inset的区域。
同时会判断InsetsState是否有更新,是否需要用心的InsetsState进行计算。
void onPostLayout() {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "ISC.onPostLayout");
for (int i = mProviders.size() - 1; i >= 0; i--) {
// 调用没有InsetsSourceProvider都调用onPostLayout布局计算,详见2.4。
mProviders.valueAt(i).onPostLayout();
}
// 前面提到过InsetsStateController有两个InsetsState,这里判断如果两个不一样,说明Insets有更新,要通知所有窗口重新计算
if (!mLastState.equals(mState)) {
mLastState.set(mState, true /* copySources */);
notifyInsetsChanged();
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
2.4 InsetsSourceProvider.onPostLayout
根据依附的窗口计算Insets位置区域等是否变化了,如果变化了则调用InsetsStateController.notifyControlChanged通知app Insets发生变化了
void onPostLayout() {
if (mWindowContainer == null) {
return;
}
// 根据依附窗口的可见性更新Insets可见性。
WindowState windowState = mWindowContainer.asWindowState();
boolean isServerVisible = windowState != null
? windowState.wouldBeVisibleIfPolicyIgnored() && windowState.isVisibleByPolicy()
: mWindowContainer.isVisibleRequested();
setServerVisible(isServerVisible);
if (mControl != null) {
boolean changed = false;
final Point position = getWindowFrameSurfacePosition();
// 依附的窗口位置变化了
if (mControl.setSurfacePosition(position.x, position.y) && mControlTarget != null) {
changed = true;
if (windowState != null && windowState.getWindowFrames().didFrameSizeChange()
&& windowState.mWinAnimator.getShown() && mWindowContainer.okToDisplay()) {
windowState.applyWithNextDraw(mSetLeashPositionConsumer);
} else {
Transaction t = mWindowContainer.getSyncTransaction();
if (windowState != null) {
final AsyncRotationController rotationController =
mDisplayContent.getAsyncRotationController();
if (rotationController != null) {
final Transaction drawT =
rotationController.getDrawTransaction(windowState.mToken);
if (drawT != null) {
t = drawT;
}
}
}
mSetLeashPositionConsumer.accept(t);
}
}
if (mServerVisible && !mLastSourceFrame.equals(mSource.getFrame())) {
final Insets insetsHint = mSource.calculateInsets(
mWindowContainer.getBounds(), true /* ignoreVisibility */);
// 计算出来的Insets区域变化了
if (!insetsHint.equals(mControl.getInsetsHint())) {
changed = true;
mControl.setInsetsHint(insetsHint);
mInsetsHint = insetsHint;
}
mLastSourceFrame.set(mSource.getFrame());
}
if (changed) {
// 通知app Insets更新,详见2.5
mStateController.notifyControlChanged(mControlTarget);
}
}
}
2.5 InsetsStateController.notifyControlChanged
void notifyControlChanged(InsetsControlTarget target) {
mPendingControlChanged.add(target);
// 详见2.6
notifyPendingInsetsControlChanged();
}
2.6 notifyPendingInsetsControlChanged
这里通知到Target一般是处在前台的窗口,所以其实每次更新只会直接通知到前台相关的几个窗口
其余的后台app是在需要切换到前台做performTraversals时候,通过relayout到WMS侧,WMS做窗口测量更新之后会把Insets返回给app侧。
private void notifyPendingInsetsControlChanged() {
if (mPendingControlChanged.isEmpty()) {
return;
}
mDisplayContent.mWmService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
for (int i = mProviders.size() - 1; i >= 0; i--) {
final InsetsSourceProvider provider = mProviders.valueAt(i);
provider.onSurfaceTransactionApplied();
}
// 这里的Target一般是处在前台的窗口,所以其实每次更新只会直接通知到前台相关的几个窗口
// 其余的后台app是在需要切换到前台做performTraversals时候,通过relayout到WMS侧,WMS做窗口测量更新之后会把Insets返回给app侧。
final ArraySet<InsetsControlTarget> newControlTargets = new ArraySet<>();
for (int i = mPendingControlChanged.size() - 1; i >= 0; i--) {
final InsetsControlTarget controlTarget = mPendingControlChanged.valueAt(i);
// 一般到app侧的是WindowState.notifyInsetsControlChanged,详见2.7
controlTarget.notifyInsetsControlChanged();
if (mControlTargetProvidersMap.containsKey(controlTarget)) {
newControlTargets.add(controlTarget);
}
}
mPendingControlChanged.clear();
for (int i = newControlTargets.size() - 1; i >= 0; i--) {
onInsetsModified(newControlTargets.valueAt(i));
}
newControlTargets.clear();
});
}
2.7 WindowState.notifyInsetsControlChanged
通过WindowState里面mClient binder到app侧,mClient是前面添加窗口流程提到的ViewRootImpl里面的W类的client侧代理。
public void notifyInsetsControlChanged() {
// 窗口还没有移除才通知
if (mRemoved) {
return;
}
final InsetsStateController stateController =
getDisplayContent().getInsetsStateController();
try {
// 这里的mClient是我们前面添加窗口流程提到的ViewRootImpl里面的W类的client侧代理,这里会binder到app,详见2.8
mClient.insetsControlChanged(getCompatInsetsState(),
stateController.getControlsForDispatch(this));
} catch (RemoteException e) {
Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e);
}
}
2.8 ViewRootImpl$W.insetsControlChanged
这里的viewAncestor就是ViewRootImpl,这个方法就是调用了ViewRootImpl.dispatchInsetsControlChanged
public void insetsControlChanged(InsetsState insetsState,
InsetsSourceControl[] activeControls) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
// 详见2.9
viewAncestor.dispatchInsetsControlChanged(insetsState, activeControls);
}
}
2.9 ViewRootImpl.dispatchInsetsControlChanged
如果不是其他进程call进来,要对activeControls参数拷贝一份,防止修改原数据。
如果兼容模式对窗口有一些缩放,这里需要对Insets也做矫正。
发送消息MSG_INSETS_CONTROL_CHANGED
private void dispatchInsetsControlChanged(InsetsState insetsState,
InsetsSourceControl[] activeControls) {
// ...如果不是其他进程call进来,要对activeControls参数拷贝一份,防止修改原数据
// 这个mTranslator前面添加窗口的流程中有提过,如果兼容模式对窗口又一些缩放等就会通过mTranslator实现,Insets也需要做同样的变化
if (mTranslator != null) {
mTranslator.translateInsetsStateInScreenToAppWindow(insetsState);
mTranslator.translateSourceControlsInScreenToAppWindow(activeControls);
}
// ...
SomeArgs args = SomeArgs.obtain();
args.arg1 = insetsState;
args.arg2 = activeControls;
详见2.10
mHandler.obtainMessage(MSG_INSETS_CONTROL_CHANGED, args).sendToTarget();
}
2.10 handleMessageImpl
调用InsetsController.onStateChanged更新端侧的Insets记录。
app侧对Insets管理结构和WMS侧差不多,ViewRootImpl里面持有一个InsetsController,里面包含两个InsetsState,一个是当前本地的InsetsState,另一个是从WMS获取更新的InsetsState,InsetsState和WMS侧的InsetsState是同一个类。
这里后续就是更新Insets,以及处理Insets变化引起的布局变化,启动变化动画等。我们主要是介绍Insets等框架以及通路,后面就不看了。
private void handleMessageImpl(Message msg) {
// ...
case MSG_INSETS_CONTROL_CHANGED: {
SomeArgs args = (SomeArgs) msg.obj;
// 调用onStateChanged,其他场景获取到Insets更新也会调用这个onStateChanged。
mInsetsController.onStateChanged((InsetsState) args.arg1);
InsetsSourceControl[] controls = (InsetsSourceControl[]) args.arg2;
if (mAdded) {
mInsetsController.onControlsChanged(controls);
} else if (controls != null) {
for (InsetsSourceControl control : controls) {
if (control != null) {
control.release(SurfaceControl::release);
}
}
}
args.recycle();
break;
}
// ...
}
小结
本节主要介绍了Insets的添加时机,以及添加后如何通知app,还有WMS和app侧对Insets管理对框架。
addWindow时候,通过layoutParams带过去Insets相关的信息,在addWindow时候会添加Insets。而当WMS出发窗口测量的方法,发现Insets有变化,就会通过binder通知当前前台的一些窗口。(这个测量方法是WMS的核心方法,我们下一节会对这个方法专门进行介绍)
WMS侧通过一个InsetsStateController对窗口进行管理,里面有两个InsetsState,相当于双缓存机制,每一个Insets的基础信息存在InsetsSource里,还有一些类用于辅助Insets和窗口的关联信息计算。app侧每个ViewRootImpl(即一个窗口)通过一个InsetsControoler进行管理,里面也是两个InsetsState,结构和WMS类似。
app侧还有一些View层的Insets计算,有比较多的细节比较繁琐,不是我们这个章节关注点,就不介绍了。
下一节会对WMS的测量窗口方法进行介绍,这个是WMS的核心方法,其中有一些计算逻辑会和Insets有关,所以我们这一节先介绍的Insets。