文章目录
前言
随着android大屏化趋势,原生的StatusBar和NavigationBar的布局位置已经不能满足现如今的需求。在车载领域,越来越多车载娱乐将android的NavigationBar布局做了调整,比较明显的一种布局方式如下图所示:将NavigationBar设计摆放在屏幕左侧。
我们这边的最终效果如上图所示,从图中我们可以拆解下Navigation Bar的修改需求
- Navigation bar布局在左侧
- Navigation Bar独占了屏幕一块区域,即应用区域在Navigation Bar的右侧区域
- Navigation Bar上边距调整
本章主要介绍Navigation Bar框架层修改的方案探索。主要分两部分介绍:
- WindowManager窗口简述:主要介绍修改点涉及到的一些关键类和关键方法。
- 方案实现细节概述:主要介绍我们的方案修改点。
一、WindowManager窗口布局简述
文章InputManagerService源码分析三:事件分发中我们了解了Window的创建过程以及ViewRootImpl的addToDisplay()方法,现在我们从ViewRootImpl的另一个方法relayoutWindow()了解下Window的测量过程。这个过程这里主要分两部分介绍,第一部分主要介绍流程进入DIsplayContent之前,第二部分主要介绍流程进入DisplayContent。
1.1 流程进入DisplayContent前
代码这里暂不一步步分析,直接粘贴出部分时序图,如下图所示。
该流程图主要描述了从客户进程中ViewRootImpl类的relayouWindow()方法,掉到DisplayContent类的applySurfaceChangesTransaction()方法为止。
1.2 流程进入DiaplayContent
下面的流程图主要介绍了调用DisplayContent类的applySurfaceChangesTransaction()方法。
其中标注了实现我们需求所涉及到的几个关键方法:
- layoutStatusBar():状态栏布局位置测算方法。
- layoutNavigationBar():导航栏布局位置测算方法。
- layoutWindowLw():一般Window布局位置测试方法。
后面一节我们会介绍如何修改这几个方法,完成我们的设计需求。
二、修改步骤
拿到设计需求,工程师分析后一般会提出如下两种开发方案,各有优劣
- 各个应用自行适配,即各个应用布局添加Margin距离
这种设计不足之处:增加了各个应用的开发成本,不利于三方应用的移植
这种设计的优点:应用层修改,不容易出错,风险可控 - Framework窗口根据设计需求裁剪Window窗口大小:
这种设计不足之处:Framework修改,风险难控
这种设计的优点:应用层无需修改,减少了应用层开发成本,便于三方app移植
本文主要介绍的是第二种方法,即从Framework层出发对Navigation Bar的位置,Window大小进行裁剪。
1. Navigation Bar左侧布局
android中Status Bar和Navigation Bar的布局位置是由WindowManager负责管理的,应用端(一般指SystemUI)是无法调整Bar的上下、左右位置的。
android中Navigation布局是会根据配置的参数设置Navigation Bar的位置,代码如下
//frameworks\base\services\core\java\com\android\server\wm\DisplayPolicy.java
private boolean layoutNavigationBar(DisplayFrames displayFrames, int uiMode, boolean navVisible,
boolean navTranslucent, boolean navAllowedHidden,
boolean statusBarForcesShowingNavigation) {
if (mNavigationBar == null) {
return false;
}
...
mNavigationBarPosition = navigationBarPosition(displayWidth, displayHeight, rotation);
...
//根据配置位置参数设置Navigation Bar的Window的位置参数
if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
...
} else if (mNavigationBarPosition == NAV_BAR_RIGHT) {
...
} else if (mNavigationBarPosition == NAV_BAR_LEFT) {
...
}
...
mNavigationBar.getWindowFrames().setFrames(navigationFrame /* parentFrame */,
navigationFrame /* displayFrame */, navigationFrame /* overscanFrame */,
displayFrames.mDisplayCutoutSafe /* contentFrame */,
navigationFrame /* visibleFrame */, sTmpRect /* decorFrame */,
navigationFrame /* stableFrame */,
displayFrames.mDisplayCutoutSafe /* outsetFrame */);
mNavigationBar.getWindowFrames().setDisplayCutout(displayFrames.mDisplayCutout);
mNavigationBar.computeFrameLw();
mNavigationBarController.setContentFrame(mNavigationBar.getContentFrameLw());
return mNavigationBarController.checkHiddenLw();
}
//frameworks\base\services\core\java\com\android\server\wm\DisplayPolicy.java
int navigationBarPosition(int displayWidth, int displayHeight, int displayRotation) {
//获取Navigation bar的配置参数
String isCustomSystemUI = SystemProperties.get("persist.systemui.custom");
if ("1".equals(isCustomSystemUI)) {
return NAV_BAR_LEFT;
}
if (navigationBarCanMove() && displayWidth > displayHeight) {
if (displayRotation == Surface.ROTATION_270) {
return NAV_BAR_LEFT;
} else if (displayRotation == Surface.ROTATION_90) {
return NAV_BAR_RIGHT;
}
}
return NAV_BAR_BOTTOM;
}
设置Navigation Bar的位置主要是通过调用layoutNavigationBar()方法设置的,其中获取Navigation Bar布局位置的方法为navigationBarPosition()方法。可以看到位置主要配置于persist.systemui.custom中。本地模拟可以通过脚本测试调整结果
adb shell setprop persist.systemui.custom 1
集成版本可以在device.mk中配置位置参数,配置如下:
具体涉及的代码位置依据平台、项目而定.这里粘贴下我们平台配置代码位置,不具有参考性
//device\semidrive\x9h\common\DeviceCommmon.mk
PRODUCT_PROPERTY_OVERRIDES += \
ro.adb.secure=1 \
persist.systemui.custom=1
2. Navigation Bar独占屏幕区域
修改Navigation Bar的位置后会发现,有两个与需求不符:
- Activity的左侧被Navigation挡住,无法看清左侧内容。
- Navigation Bar占据了Status Bar的位置,没有按照需求留有一定距离的上边距离
这里我们通过layoutWindowLw()方法,分析NavigationBar布局对应用Activity边衬距离的影响。
//frameworks\base\services\core\java\com\android\server\wm\DisplayPolicy.java
public void layoutWindowLw(WindowState win, WindowState attached, DisplayFrames displayFrames) {
...
//Window各种位置信息,其中我们关心的是边衬距离
final Rect pf = windowFrames.mParentFrame;
final Rect df = windowFrames.mDisplayFrame;
final Rect of = windowFrames.mOverscanFrame;
final Rect cf = windowFrames.mContentFrame;
final Rect vf = windowFrames.mVisibleFrame;
final Rect dcf = windowFrames.mDecorFrame;
final Rect sf = windowFrames.mStableFrame;
dcf.setEmpty();
windowFrames.setParentFrameWasClippedByDisplayCutout(false);
windowFrames.setDisplayCutout(displayFrames.mDisplayCutout);
...
sf.set(displayFrames.mStable);
if (type == TYPE_INPUT_METHOD) {
...
} else if (type == TYPE_VOICE_INTERACTION) {
...
} else if (type == TYPE_WALLPAPER) {
layoutWallpaper(displayFrames, pf, df, of, cf);
} else if (win == mStatusBar) {
of.set(displayFrames.mUnrestricted);
df.set(displayFrames.mUnrestricted);
pf.set(displayFrames.mUnrestricted);
cf.set(displayFrames.mStable);
vf.set(displayFrames.mStable);
if (adjust == SOFT_INPUT_ADJUST_RESIZE) {
cf.bottom = displayFrames.mContent.bottom;
} else {
cf.bottom = displayFrames.mDock.bottom;
vf.bottom = displayFrames.mContent.bottom;
}
} else {
...
if (layoutInScreen && layoutInsetDecor) {
//Activiy走这个流程
...
} else if (layoutInScreen || (sysUiFl
& (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)) != 0) {
if (DEBUG_LAYOUT) Slog.v(TAG, "layoutWindowLw(" + attrs.getTitle()
+ "): IN_SCREEN");
// A window that has requested to fill the entire screen just
// gets everything, period.
if (type == TYPE_STATUS_BAR_PANEL || type == TYPE_STATUS_BAR_SUB_PANEL) {
...
} else if (type == TYPE_NAVIGATION_BAR || type == TYPE_NAVIGATION_BAR_PANEL) {
...
} else if ((type == TYPE_SECURE_SYSTEM_OVERLAY || type == TYPE_SCREENSHOT)
&& ((fl & FLAG_FULLSCREEN) != 0)) {
...
} else if (type == TYPE_BOOT_PROGRESS) {
...
} else if ((fl & FLAG_LAYOUT_IN_OVERSCAN) != 0
&& type >= FIRST_APPLICATION_WINDOW && type <= LAST_SUB_WINDOW) {
...
} else if ((sysUiFl & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0
&& (type == TYPE_STATUS_BAR
|| type == TYPE_TOAST
|| type == TYPE_DOCK_DIVIDER
|| type == TYPE_VOICE_INTERACTION_STARTING
|| (type >= FIRST_APPLICATION_WINDOW && type <= LAST_SUB_WINDOW))) {
...
} else if ((sysUiFl & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0) {
...
} else {
cf.set(displayFrames.mRestricted);
of.set(displayFrames.mRestricted);
df.set(displayFrames.mRestricted);
pf.set(displayFrames.mRestricted);
}
applyStableConstraints(sysUiFl, fl, cf, displayFrames);
if (adjust != SOFT_INPUT_ADJUST_NOTHING) {
vf.set(displayFrames.mCurrent);
} else {
vf.set(cf);
}
} else if (attached != null) {
...
} else {
...
}
}
...
//may occur errors for window's region
cf.left = getNavigationBarWidth(displayFrames.mRotation, mService.mPolicy.getUiMode());
if (DEBUG_LAYOUT) Slog.v(TAG, "Compute frame " + attrs.getTitle()
+ ": sim=#" + Integer.toHexString(sim)
+ " attach=" + attached + " type=" + type
+ String.format(" flags=0x%08x", fl)
+ " pf=" + pf.toShortString() + " df=" + df.toShortString()
+ " of=" + of.toShortString()
+ " of=" + of.toShortString()
+ " cf=" + cf.toShortString() + " vf=" + vf.toShortString()
+ " dcf=" + dcf.toShortString()
+ " sf=" + sf.toShortString()
+ " osf=" + windowFrames.mOutsetFrame.toShortString() + " " + win);
if (!sTmpLastParentFrame.equals(pf)) {
windowFrames.setContentChanged(true);
}
win.computeFrameLw();
// Dock windows carve out the bottom of the screen, so normal windows
// can't appear underneath them.
if (type == TYPE_INPUT_METHOD && win.isVisibleLw()
&& !win.getGivenInsetsPendingLw()) {
offsetInputMethodWindowLw(win, displayFrames);
}
if (type == TYPE_VOICE_INTERACTION && win.isVisibleLw()
&& !win.getGivenInsetsPendingLw()) {
offsetVoiceInputWindowLw(win, displayFrames);
}
}
从代码中可以看出WindowManager会根据各个Window的属性计算各个Window的位置信息,这些位置信息会影响我们应用Window的位置,Window的边衬距离。这里我们主要通过修改如下一句代码实现Navigation Bar独占屏幕左侧,Activity左边添加Margin。
//frameworks\base\services\core\java\com\android\server\wm\DisplayPolicy.java
cf.left = getNavigationBarWidth(displayFrames.mRotation, mService.mPolicy.getUiMode());
关于验证:
我们主要通过如下三种方式验证我们修改方式的正确性:
1.车机应用以Activity为载体的页面显示正常;
2.三方游戏(隐藏状态栏)无左边距,亦显示正常;
3.三方其他应用出现过问题,可以调整Activity的样式解决。
关于第3点出现的问题和修改方式,因为涉及到平台化的问题,比较重要,我们单独拉出一章节来介绍。
这种修改比较粗糙,为全局性修改,实际情况我们只需修改Activity的边衬距离即可。后续出现问题,我们即可以按照这种思路来解决问题。
3. Navigation Bar边距调节
上一节主要通过layoutWindowLw()我们了解了普通Window位置信息的处理流程。本节以layoutNavigationBar()方法为了解下修改Navigation Bar上面距的方法。layoutNavigationBar()方法如下:
private boolean layoutNavigationBar(DisplayFrames displayFrames, int uiMode, boolean navVisible,
boolean navTranslucent, boolean navAllowedHidden,
boolean statusBarForcesShowingNavigation) {
if (mNavigationBar == null) {
return false;
}
final Rect navigationFrame = sTmpNavFrame;
boolean transientNavBarShowing = mNavigationBarController.isTransientShowing();
// Force the navigation bar to its appropriate place and size. We need to do this directly,
// instead of relying on it to bubble up from the nav bar, because this needs to change
// atomically with screen rotations.
final int rotation = displayFrames.mRotation;
final int displayHeight = displayFrames.mDisplayHeight;
final int displayWidth = displayFrames.mDisplayWidth;
final Rect dockFrame = displayFrames.mDock;
mNavigationBarPosition = navigationBarPosition(displayWidth, displayHeight, rotation);
final Rect cutoutSafeUnrestricted = sTmpRect;
cutoutSafeUnrestricted.set(displayFrames.mUnrestricted);
cutoutSafeUnrestricted.intersectUnchecked(displayFrames.mDisplayCutoutSafe);
if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
...
} else if (mNavigationBarPosition == NAV_BAR_RIGHT) {
...
} else if (mNavigationBarPosition == NAV_BAR_LEFT) {
// Seascape screen; nav bar goes to the left.
final int right = cutoutSafeUnrestricted.left
+ getNavigationBarWidth(rotation, uiMode);
//这里修改Navigation Bar的上边距
boolean isStatusBarHide = !mStatusBar.isVisibleLw();
int marginTop = isStatusBarHide?0:getStatusBarHeight(displayFrames);
navigationFrame.set(displayFrames.mUnrestricted.left, marginTop, right,displayHeight);
displayFrames.mStable.left = displayFrames.mStableFullscreen.left = right;
if (transientNavBarShowing) {
mNavigationBarController.setBarShowingLw(true);
} else if (navVisible) {
mNavigationBarController.setBarShowingLw(true);
dockFrame.left = displayFrames.mRestricted.left =
displayFrames.mRestrictedOverscan.left = right;
} else {
// We currently want to hide the navigation UI - unless we expanded the status bar.
mNavigationBarController.setBarShowingLw(statusBarForcesShowingNavigation);
}
if (navVisible && !navTranslucent && !navAllowedHidden
&& !mNavigationBar.isAnimatingLw()
&& !mNavigationBarController.wasRecentlyTranslucent()) {
// If the nav bar is currently requested to be visible, and not in the process of
// animating on or off, then we can tell the app that it is covered by it.
displayFrames.mSystem.left = right;
}
}
// Make sure the content and current rectangles are updated to account for the restrictions
// from the navigation bar.
displayFrames.mCurrent.set(dockFrame);
displayFrames.mVoiceContent.set(dockFrame);
displayFrames.mContent.set(dockFrame);
// And compute the final frame.
sTmpRect.setEmpty();
mNavigationBar.getWindowFrames().setFrames(navigationFrame /* parentFrame */,
navigationFrame /* displayFrame */, navigationFrame /* overscanFrame */,
displayFrames.mDisplayCutoutSafe /* contentFrame */,
navigationFrame /* visibleFrame */, sTmpRect /* decorFrame */,
navigationFrame /* stableFrame */,
displayFrames.mDisplayCutoutSafe /* outsetFrame */);
mNavigationBar.getWindowFrames().setDisplayCutout(displayFrames.mDisplayCutout);
mNavigationBar.computeFrameLw();
mNavigationBarController.setContentFrame(mNavigationBar.getContentFrameLw());
if (DEBUG_LAYOUT) Slog.i(TAG, "mNavigationBar frame: " + navigationFrame);
return mNavigationBarController.checkHiddenLw();
}
我们通过如下代码修改NaviationBar的上边距,我们需要根据状态栏的显示与否动态更新Navigation上边距。
boolean isStatusBarHide = !mStatusBar.isVisibleLw();
int marginTop = isStatusBarHide?0:getStatusBarHeight(displayFrames);
navigationFrame.set(displayFrames.mUnrestricted.left, marginTop, right, displayHeight);
三、注意事项
原生的android应用端可以设置Navigation Bar透明,经测试会与我们修改策略冲突,表现为:应用被Navigation Bar遮挡,即我们修改的策略不生效。需要做如下两种修改:
- 代码不能将Navigation设置为透明;
- styles.xml样式不能设置透明,即android:windowTranslucentNavigation为false.
<item name="android:windowTranslucentNavigation">false</item>
总结
我们Navigation Bar左侧布局方案介绍就到这里,方案只涉及到一部分WindowManager的管理,后续我们会详细介绍下WIndowManagerService。