前言
在使用Android12为车机系统载体进行系统SystemUI开发的过程中发现一个很奇特的问题,当不同页面发生切换的时候,导航栏总是会闪一下,其实就是窗口焦点发生变化的时候,导航栏总是会消失一下再出现,虽然问题不是很严重,但这对于用户体验来说是极差的,本篇文章我们就来梳理一下为什么会出现这种现象。
一、窗口焦点发生变化
1、当窗口焦点发生变化的时候,首先会触发WindowManagerService的updateFocusedWindowLocked方法。
frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
public class WindowManagerService extends IWindowManager.Stub implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs {
RootWindowContainer mRoot;
boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "wmUpdateFocus");
boolean changed = mRoot.updateFocusedWindowLocked(mode, updateInputWindows);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
return changed;
}
}
WindowManagerService的updateFocusedWindowLocked方法会进一步调用RootWindowContainer的updateFocusedWindowLocked方法。
2、RootWindowContainer的updateFocusedWindowLocked方法如下所示。
frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java
class RootWindowContainer extends WindowContainer<DisplayContent>
implements DisplayManager.DisplayListener {
// List of children for this window container. List is in z-order as the children appear on
// screen with the top-most window container at the tail of the list.
protected final WindowList<E> mChildren = new WindowList<E>();//注意,此属性是父类WindowContainer的属性
boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) {
mTopFocusedAppByProcess.clear();
boolean changed = false;
int topFocusedDisplayId = INVALID_DISPLAY;
for (int i = mChildren.size() - 1; i >= 0; --i) {
final DisplayContent dc = mChildren.get(i);
//循环调用集合子条目DisplayContent的updateFocusedWindowLocked方法
changed |= dc.updateFocusedWindowLocked(mode, updateInputWindows, topFocusedDisplayId);
final WindowState newFocus = dc.mCurrentFocus;
if (newFocus != null) {
final int pidOfNewFocus = newFocus.mSession.mPid;
if (mTopFocusedAppByProcess.get(pidOfNewFocus) == null) {
mTopFocusedAppByProcess.put(pidOfNewFocus, newFocus.mActivityRecord);
}
if (topFocusedDisplayId == INVALID_DISPLAY) {
topFocusedDisplayId = dc.getDisplayId();
}
} else if (topFocusedDisplayId == INVALID_DISPLAY && dc.mFocusedApp != null) {
// The top-most display that has a focused app should still be the top focused
// display even when the app window is not ready yet (process not attached or
// window not added yet).
topFocusedDisplayId = dc.getDisplayId();
}
}
if (topFocusedDisplayId == INVALID_DISPLAY) {
topFocusedDisplayId = DEFAULT_DISPLAY;
}
if (mTopFocusedDisplayId != topFocusedDisplayId) {
mTopFocusedDisplayId = topFocusedDisplayId;
mWmService.mInputManager.setFocusedDisplay(topFocusedDisplayId);
mWmService.mPolicy.setTopFocusedDisplay(topFocusedDisplayId);
mWmService.mAccessibilityController.setFocusedDisplay(topFocusedDisplayId);
ProtoLog.d(WM_DEBUG_FOCUS_LIGHT, "New topFocusedDisplayId=%d", topFocusedDisplayId);
}
return changed;
}
}
RootWindowContainer 是屏幕设备的根容器管理者,子容器是 DisplayContent,每个DisplayContent代表一个屏幕设备的显示区域,
RootWindowContainer的updateFocusedWindowLocked方法会循环调用子容器DisplayContent的updateFocusedWindowLocked方法,通知各个屏幕设备窗口焦点发生了变化。
3、DisplayContent的updateFocusedWindowLocked方法如下所示。
frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java
class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.DisplayContentInfo {
/**
* 如果窗口焦点发生了变化,会调用此方法对焦点窗口做一些参数调整*
* @param mode Indicates the situation we are in. Possible modes are:
* {@link WindowManagerService#UPDATE_FOCUS_NORMAL},
* {@link WindowManagerService#UPDATE_FOCUS_PLACING_SURFACES},
* {@link WindowManagerService#UPDATE_FOCUS_WILL_PLACE_SURFACES},
* {@link WindowManagerService#UPDATE_FOCUS_REMOVING_FOCUS}
* @param updateInputWindows Whether to sync the window information to the input module.
* @param topFocusedDisplayId Display id of current top focused display.
* @return {@code true} if the focused window has changed.
*/
boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows, int topFocusedDisplayId) {
//获取顶部焦点设备的焦点窗口
WindowState newFocus = findFocusedWindowIfNeeded(topFocusedDisplayId);
if (mCurrentFocus == newFocus) {//如果窗口焦点没有变化直接返回false
return false;
}
boolean imWindowChanged = false;
final WindowState imWindow = mInputMethodWindow;
if (imWindow != null) {
final WindowState prevTarget = mImeLayeringTarget;
final WindowState newTarget = computeImeTarget(true /* updateImeTarget*/);
imWindowChanged = prevTarget != newTarget;
if (mode != UPDATE_FOCUS_WILL_ASSIGN_LAYERS
&& mode != UPDATE_FOCUS_WILL_PLACE_SURFACES) {
assignWindowLayers(false /* setLayoutNeeded */);
}
if (imWindowChanged) {
mWmService.mWindowsChanged = true;
setLayoutNeeded();
newFocus = findFocusedWindowIfNeeded(topFocusedDisplayId);
}
}
ProtoLog.d(WM_DEBUG_FOCUS_LIGHT, "Changing focus from %s to %s displayId=%d Callers=%s",
mCurrentFocus, newFocus, getDisplayId(), Debug.getCallers(4));
final WindowState oldFocus = mCurrentFocus;
mCurrentFocus = newFocus;
if (newFocus != null) {
mWinAddedSinceNullFocus.clear();
mWinRemovedSinceNullFocus.clear();
if (newFocus.canReceiveKeys()) {
// Displaying a window implicitly causes dispatching to be unpaused.
// This is to protect against bugs if someone pauses dispatching but
// forgets to resume.
newFocus.mToken.paused = false;
}
}
//通知屏幕窗口策略对象设备焦点发生了变化
getDisplayPolicy().focusChangedLw(oldFocus, newFocus);
if (imWindowChanged && oldFocus != mInputMethodWindow) {
// Focus of the input method window changed. Perform layout if needed.
if (mode == UPDATE_FOCUS_PLACING_SURFACES) {
//准备重新布局
performLayout(true /*initial*/, updateInputWindows);
} else if (mode == UPDATE_FOCUS_WILL_PLACE_SURFACES) {
// Client will do the layout, but we need to assign layers
// for handleNewWindowLocked() below.
assignWindowLayers(false /* setLayoutNeeded */);
}
}
if (mode != UPDATE_FOCUS_WILL_ASSIGN_LAYERS) {
// If we defer assigning layers, then the caller is responsible for doing this part.
getInputMonitor().setInputFocusLw(newFocus, updateInputWindows);
}
//如果需要的话会对输入法窗口参数做调整
adjustForImeIfNeeded();
// We may need to schedule some toast windows to be removed. The toasts for an app that
// does not have input focus are removed within a timeout to prevent apps to redress
// other apps' UI.
scheduleToastWindowsTimeoutIfNeededLocked(oldFocus, newFocus);
if (mode == UPDATE_FOCUS_PLACING_SURFACES) {
pendingLayoutChanges |= FINISH_LAYOUT_REDO_ANIM;
}
// Notify the accessibility manager for the change so it has the windows before the newly
// focused one starts firing events.
// TODO(b/151179149) investigate what info accessibility service needs before input can
// dispatch focus to clients.
if (mWmService.mAccessibilityController.hasCallbacks()) {
mWmService.mH.sendMessage(PooledLambda.obtainMessage(
this::updateAccessibilityOnWindowFocusChanged,
mWmService.mAccessibilityController));
}
return true;
}
}
DisplayContent的updateFocusedWindowLocked方法会进一步调用屏幕窗口策略对象DisplayPolicy的focusChangedLw方法。
4、DisplayPolicy的focusChangedLw方法如下所示。
frameworks/base/services/core/java/com/android/server/wm/DisplayPolicy.java
public class DisplayPolicy {
/**
* A new window has been focused.
*/
public void focusChangedLw(WindowState lastFocus, WindowState newFocus) {
//焦点发生了变化
mFocusedWindow = newFocus;
mLastFocusedWindow = lastFocus;
if (mDisplayContent.isDefaultDisplay) {
mService.mPolicy.onDefaultDisplayFocusChangedLw(newFocus);
}
//更新SystemBar的属性
updateSystemBarAttributes();
}
}
DisplayPolicy的focusChangedLw方法最终会调用updateSystemBarAttributes方法来刷新系统栏属性。
二、更新系统栏属性
1、DisplayPolicy的updateSystemBarAttributes的方法如下所示。
public class DisplayPolicy {
//更新SystemBar的属性
void updateSystemBarAttributes() {
WindowState winCandidate = mFocusedWindow;//焦点窗口
if (winCandidate == null && mTopFullscreenOpaqueWindowState != null
&& (mTopFullscreenOpaqueWindowState.mAttrs.flags
& WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) == 0) {
//只有焦点窗口才能控制系统栏
winCandidate = mTopFullscreenOpaqueWindowState;
}
// If there is no window focused, there will be nobody to handle the events
// anyway, so just hang on in whatever state we're in until things settle down.
if (winCandidate == null) {
return;
}
// The immersive mode confirmation should never affect the system bar visibility, otherwise
// it will unhide the navigation bar and hide itself.
if (winCandidate.getAttrs().token == mImmersiveModeConfirmation.getWindowToken()) {
// The immersive mode confirmation took the focus from mLastFocusedWindow which was
// controlling the system ui visibility. So if mLastFocusedWindow can still receive
// keys, we let it keep controlling the visibility.
final boolean lastFocusCanReceiveKeys =
(mLastFocusedWindow != null && mLastFocusedWindow.canReceiveKeys());
winCandidate = isKeyguardShowing() && !isKeyguardOccluded() ? mNotificationShade
: lastFocusCanReceiveKeys ? mLastFocusedWindow
: mTopFullscreenOpaqueWindowState;
if (winCandidate == null) {
return;
}
}
final WindowState win = winCandidate;
mSystemUiControllingWindow = win;
final int displayId = getDisplayId();//获取屏幕设备id
final int disableFlags = win.getDisableFlags();//获取窗口禁止的属性标记
//结合参数disableFlags重新更新系统栏窗口参数
final int opaqueAppearance = updateSystemBarsLw(win, disableFlags);
final WindowState navColorWin = chooseNavigationColorWindowLw(mNavBarColorWindowCandidate,
mDisplayContent.mInputMethodWindow, mNavigationBarPosition);
final boolean isNavbarColorManagedByIme =
navColorWin != null && navColorWin == mDisplayContent.mInputMethodWindow;
final int appearance = updateLightNavigationBarLw(win.mAttrs.insetsFlags.appearance,
navColorWin) | opaqueAppearance;
final int behavior = win.mAttrs.insetsFlags.behavior;
final String focusedApp = win.mAttrs.packageName;
final boolean isFullscreen = !win.getRequestedVisibility(ITYPE_STATUS_BAR)
|| !win.getRequestedVisibility(ITYPE_NAVIGATION_BAR);
final AppearanceRegion[] appearanceRegions =
new AppearanceRegion[mStatusBarColorWindows.size()];
for (int i = mStatusBarColorWindows.size() - 1; i >= 0; i--) {
final WindowState windowState = mStatusBarColorWindows.get(i);
appearanceRegions[i] = new AppearanceRegion(
getStatusBarAppearance(windowState, windowState),
new Rect(windowState.getFrame()));
}
if (mLastDisableFlags != disableFlags) {
mLastDisableFlags = disableFlags;
final String cause = win.toString();
callStatusBarSafely(statusBar -> statusBar.setDisableFlags(displayId, disableFlags,
cause));
}
if (mLastAppearance == appearance
&& mLastBehavior == behavior
&& mRequestedVisibilities.equals(win.getRequestedVisibilities())
&& Objects.equals(mFocusedApp, focusedApp)
&& mLastFocusIsFullscreen == isFullscreen
&& Arrays.equals(mLastStatusBarAppearanceRegions, appearanceRegions)) {
return;
}
if (mDisplayContent.isDefaultDisplay && mLastFocusIsFullscreen != isFullscreen
&& ((mLastAppearance ^ appearance) & APPEARANCE_LOW_PROFILE_BARS) != 0) {
mService.mInputManager.setSystemUiLightsOut(
isFullscreen || (appearance & APPEARANCE_LOW_PROFILE_BARS) != 0);
}
final InsetsVisibilities requestedVisibilities =
new InsetsVisibilities(win.getRequestedVisibilities());
mLastAppearance = appearance;
mLastBehavior = behavior;
mRequestedVisibilities = requestedVisibilities;
mFocusedApp = focusedApp;
mLastFocusIsFullscreen = isFullscreen;
mLastStatusBarAppearanceRegions = appearanceRegions;
//触发状态栏管理服务StatusBarManagerService的onSystemBarAttributesChanged
callStatusBarSafely(statusBar -> statusBar.onSystemBarAttributesChanged(displayId,
appearance, appearanceRegions, isNavbarColorManagedByIme, behavior,
requestedVisibilities, focusedApp));
}
}
updateSystemBarAttributes方法首先获取当前的焦点窗口mFocusedWindow,将该窗口赋值给winCandidate,并判断该窗口是否可以操控系统栏,如果不允许会直接返回;如果允许,则会结合winCandidate的属性调用updateSystemBarsLw方法,来更新系统栏参数。
2、DisplayPolicy的updateSystemBarsLw方法如下所示。
public class DisplayPolicy {
private int updateSystemBarsLw(WindowState win, int disableFlags) {
final TaskDisplayArea defaultTaskDisplayArea = mDisplayContent.getDefaultTaskDisplayArea();
final boolean multiWindowTaskVisible =
defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY)
|| defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_MULTI_WINDOW);
final boolean freeformRootTaskVisible =
defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_FREEFORM);
//判断当前是否存在多任务窗口或者悬浮窗,如果存在则需要强制显示系统栏
mForceShowSystemBars = multiWindowTaskVisible || freeformRootTaskVisible;
//经测试发现,窗口焦点变化的时候,这行代码会触发导航栏闪一下
mDisplayContent.getInsetsPolicy().updateBarControlTarget(win);
//导航栏或状态栏不透明属性
int appearance = APPEARANCE_OPAQUE_NAVIGATION_BARS | APPEARANCE_OPAQUE_STATUS_BARS;
appearance = configureStatusBarOpacity(appearance);
appearance = configureNavBarOpacity(appearance, multiWindowTaskVisible,
freeformRootTaskVisible);
//是否需要隐藏导航栏
final boolean requestHideNavBar = !win.getRequestedVisibility(ITYPE_NAVIGATION_BAR);
final long now = SystemClock.uptimeMillis();
final boolean pendingPanic = mPendingPanicGestureUptime != 0
&& now - mPendingPanicGestureUptime <= PANIC_GESTURE_EXPIRATION;
final DisplayPolicy defaultDisplayPolicy =
mService.getDefaultDisplayContentLocked().getDisplayPolicy();
if (pendingPanic && requestHideNavBar && win != mNotificationShade
&& getInsetsPolicy().isHidden(ITYPE_NAVIGATION_BAR)
// TODO (b/111955725): Show keyguard presentation on all external displays
&& defaultDisplayPolicy.isKeyguardDrawComplete()) {
// The user performed the panic gesture recently, we're about to hide the bars,
// we're no longer on the Keyguard and the screen is ready. We can now request the bars.
mPendingPanicGestureUptime = 0;
if (!isNavBarEmpty(disableFlags)) {
mDisplayContent.getInsetsPolicy().showTransient(SHOW_TYPES_FOR_PANIC,
true /* isGestureOnSystemBar */);
}
}
// update navigation bar
boolean oldImmersiveMode = mLastImmersiveMode;
boolean newImmersiveMode = isImmersiveMode(win);
if (oldImmersiveMode != newImmersiveMode) {
mLastImmersiveMode = newImmersiveMode;
// The immersive confirmation window should be attached to the immersive window root.
final RootDisplayArea root = win.getRootDisplayArea();
final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId;
mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId, newImmersiveMode,
mService.mPolicy.isUserSetupComplete(),
isNavBarEmpty(disableFlags));
}
return appearance;
}
}
以上代码经过测试发现,是mDisplayContent.getInsetsPolicy().updateBarControlTarget(win)这段代码导致窗口焦点发生变化的时候,导航栏会闪一下。
3、InsetsPolicy调用updateBarControlTarget更新当前可以控制系统栏的窗口对象。
frameworks/base/services/core/java/com/android/server/wm/InsetsPolicy.java
class InsetsPolicy {
/** Updates the target which can control system bars. */
void updateBarControlTarget(@Nullable WindowState focusedWin) {
if (mFocusedWin != focusedWin){
abortTransient();
}
mFocusedWin = focusedWin;
final InsetsControlTarget statusControlTarget =
getStatusControlTarget(focusedWin, false /* fake */);
final InsetsControlTarget navControlTarget =
getNavControlTarget(focusedWin, false /* fake */);
mStateController.onBarControlTargetChanged(
statusControlTarget,
statusControlTarget == mDummyControlTarget
? getStatusControlTarget(focusedWin, true /* fake */)
: null,
navControlTarget,
navControlTarget == mDummyControlTarget
? getNavControlTarget(focusedWin, true /* fake */)
: null);
mStatusBar.updateVisibility(statusControlTarget, ITYPE_STATUS_BAR);
mNavBar.updateVisibility(navControlTarget, ITYPE_NAVIGATION_BAR);
}
}
InsetsPolicy的updateBarControlTarget方法的主要目的是更新当前可以控制系统栏的窗口对象,此方法会进一步调用InsetsStateController的onBarControlTargetChanged方法。
4、InsetsStateController的onBarControlTargetChanged方法如下所示。
frameworks/base/services/core/java/com/android/server/wm/InsetsStateController.java
class InsetsStateController {
/**
* Called when the focused window that is able to control the system bars changes.
*
* @param statusControlling The target that is now able to control the status bar appearance
* and visibility.
* @param navControlling The target that is now able to control the nav bar appearance
* and visibility.
*/
void onBarControlTargetChanged(@Nullable InsetsControlTarget statusControlling,
@Nullable InsetsControlTarget fakeStatusControlling,
@Nullable InsetsControlTarget navControlling,
@Nullable InsetsControlTarget fakeNavControlling) {
onControlChanged(ITYPE_STATUS_BAR, statusControlling);//状态栏
onControlChanged(ITYPE_NAVIGATION_BAR, navControlling);//导航栏
onControlChanged(ITYPE_CLIMATE_BAR, statusControlling);
onControlChanged(ITYPE_EXTRA_NAVIGATION_BAR, navControlling);
onControlFakeTargetChanged(ITYPE_STATUS_BAR, fakeStatusControlling);
onControlFakeTargetChanged(ITYPE_NAVIGATION_BAR, fakeNavControlling);
onControlFakeTargetChanged(ITYPE_CLIMATE_BAR, fakeStatusControlling);
onControlFakeTargetChanged(ITYPE_EXTRA_NAVIGATION_BAR, fakeNavControlling);
notifyPendingInsetsControlChanged();
}
}
InsetsStateController的onBarControlTargetChanged方法会触发onControlChanged方法。
5、InsetsStateController的onControlChanged方法如下所示。
class InsetsStateController {
private void onControlChanged(@InternalInsetsType int type,
@Nullable InsetsControlTarget target) {
//获取原来的系统栏控制对象
final InsetsControlTarget previous = mTypeControlTargetMap.get(type);
//如果新的系统栏控制对象和原来的相同直接返回
if (target == previous) {
return;
}
//获取系统栏提供者
final InsetsSourceProvider provider = mProviders.get(type);
//如果为空直接返回
if (provider == null) {
return;
}
//如果系统栏提供者不可以控制对应的系统栏也直接返回
if (!provider.isControllable()) {
return;
}
//系统栏提供者更新系统栏控制者对象
provider.updateControlForTarget(target, false /* force */);
target = provider.getControlTarget();
if (previous != null) {//如果原来的系统栏控制者对象不为空
//将其从
removeFromControlMaps(previous, type, false /* fake */);
mPendingControlChanged.add(previous);
}
if (target != null) {
addToControlMaps(target, type, false /* fake */);
mPendingControlChanged.add(target);
}
}
}
InsetsStateController的onControlChanged方法中会调用InsetsControlTarget的updateControlForTarget方法。
6、InsetsStateController的updateControlForTarget方法如下所示。
frameworks/base/services/core/java/com/android/server/wm/InsetsStateController.java
class InsetsSourceProvider {
void updateControlForTarget(@Nullable InsetsControlTarget target, boolean force) {
if (mSeamlessRotating) {
// We are un-rotating the window against the display rotation. We don't want the target
// to control the window for now.
return;
}
if (mWin != null && mWin.getSurfaceControl() == null) {
// if window doesn't have a surface, set it null and return.
setWindow(null, null, null);
}
if (mWin == null) {
mPendingControlTarget = target;
return;
}
if (target == mControlTarget && !force) {
return;
}
if (target == null) {
// Cancelling the animation will invoke onAnimationCancelled, resetting all the fields.
mWin.cancelAnimation();
setClientVisible(InsetsState.getDefaultVisibility(mSource.getType()));
return;
}
final Point surfacePosition = getWindowFrameSurfacePosition();
mAdapter = new ControlAdapter(surfacePosition);
if (getSource().getType() == ITYPE_IME) {
//设置客户端的可见性
setClientVisible(target.getRequestedVisibility(mSource.getType()));
}
final Transaction t = mDisplayContent.getSyncTransaction();
//开启动画,就是这个动画触发闪烁的
mWin.startAnimation(t, mAdapter, !mClientVisible /* hidden */,
ANIMATION_TYPE_INSETS_CONTROL);
// The leash was just created. We cannot dispatch it until its surface transaction is
// applied. Otherwise, the client's operation to the leash might be overwritten by us.
mIsLeashReadyForDispatching = false;
final SurfaceControl leash = mAdapter.mCapturedLeash;
mControlTarget = target;
updateVisibility();
mControl = new InsetsSourceControl(mSource.getType(), leash, surfacePosition,
mSource.calculateInsets(mWin.getBounds(), true /* ignoreVisibility */));
ProtoLog.d(WM_DEBUG_WINDOW_INSETS,
"InsetsSource Control %s for target %s", mControl, mControlTarget);
}
}
经过测试最终发现,就是mWin.startAnimation这段代码触发底部栏闪烁的。高版本的系统在窗口焦点发生变化的时候,会首先清除上一个窗口设置给状态栏和导航栏的相关属性,将导航栏变为不可见,然后再可见。
二、解决方案
解决方案有两种:
1、找到这个动画的源头,修改这个动画的逻辑。
2、在触发mWin.startAnimation这段代码的时候做一下过滤,过滤掉导航栏。
class InsetsSourceProvider {
void updateControlForTarget(@Nullable InsetsControlTarget target, boolean force) {
...代码省略...
if (getSource().getType() != ITYPE_NAVIGATION_BAR) {
//开启动画,就是这个动画触发闪烁的,所以这里我们过滤掉导航栏
mWin.startAnimation(t, mAdapter, !mClientVisible /* hidden */,
ANIMATION_TYPE_INSETS_CONTROL);
}
...代码省略...
}
}
💡 技术无价,赞赏随心
写文不易,如果本文帮你避开了“八小时踩坑”,或者让你直呼“学到了!”
欢迎扫码赞赏,让我知道这篇内容值得!