什么是LetterBox
应用可以配置保持固定的大小或方向,而不考虑显示大小或设备方向。如果一个应用请求固定的方向或是不可调整大小的,并且其最大或最小比例与设备的显示比例不兼容,应用就会以LetterBox模式打开。
LetterBox通常出现在大屏幕设备上,尤其是可折叠设备上,因为设备的显示尺寸和宽高比通常与标准手机不同,而大多数应用程序都是为标准手机设计的。
哪些情况会触发LetterBox显示
当应用程序无法调整大小或它们具有固定方向时,就可能会发生Letterboxing。控制应用程序方向和可调整性的配置包括下面这些:
- screenOrientation: 为应用程序指定固定的方向,也可以在运行时使用
Activity#setRequestedOrientation()
方法设置 - resizeableActivity:当应用声明不可resize并且声明的宽高比与容器不兼容的时候(如屏幕宽高超出android:maxAspectRatio、minAspectRatio)
- setIgnoreOrientationRequest(true) 这是系统设置忽略屏幕方向后,以横屏模式下打开一个强制竖屏的界面
可以使用adb命令验证:
adb shell wm set-ignore-orientation-request true
LetterBox模式打开微信效果演示
Display 4630946545580055170 HWC layers:
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Layer name
Z | Window Type | Layer Class | Comp Type | Transform | Disp Frame (LTRB) | Source Crop (LTRB) | Frame Rate (Explicit) (Seamlessness) [Focused]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Letterbox - left#3
rel -1 | 0 | 0 | SOLID_COLOR | ROT_270 | 0 1782 1080 2400 | 0.0 0.0 0.0 0.0 | [ ]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Letterbox - right#3
rel -1 | 0 | 0 | SOLID_COLOR | ROT_270 | 0 0 1080 702 | 0.0 0.0 0.0 0.0 | [ ]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
com.tencent.mm/com.tencent.mm.ui.LauncherUI#0
rel 0 | 1 | 0 | DEVICE | 0 | 0 702 1080 1782 | 0.0 0.0 1080.0 1080.0 | [*]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
StatusBar#0
rel 0 | 2000 | 0 | DEVICE | 0 | 0 0 84 2400 | 0.0 0.0 84.0 2400.0 | [ ]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
NavigationBar0#0
rel 0 | 2019 | 0 | DEVICE | 0 | 1036 0 1080 2400 | 0.0 0.0 44.0 2400.0 | [ ]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
RoundCornerTop#0
rel 0 | 2024 | 0 | DEVICE | 0 | 0 0 1080 90 | 0.0 0.0 1080.0 90.0 | [ ]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
RoundCornerBottom#0
rel 0 | 2024 | 0 | DEVICE | 0 | 0 2310 1080 2400 | 0.0 0.0 1080.0 90.0 | [ ]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Letterbox - left#3和Letterbox - right#3就是微信左右两边填充的黑色区域。
Android 12/12L对LetterBox的美化增强
API level 31 引入的:
- 圆角:应用程序窗口的角具有更精致的外观。
- 状态栏透明度:覆盖应用程序的状态栏是半透明的,使应用程序窗口顶部和底部边缘的元素可见。
- 可配置的纵横比:设备厂商可以调整应用的纵横比以改善其外观。
API level 32 将要引入的:
可配置的位置:在大屏幕上,设备厂商可以配置应用程序定位在屏幕的左侧或右侧,使交互更容易。
重新设计的重启按钮:设备制造商可以给重启按钮的尺寸兼容模式定制一个新的外观,以更好的被用户识别。
上代码
Android S引入了新的LetterboxUiController类控制LetterBox
流程入口
整个流程开始于ApplySurfaceChangesTransaction,
从activity.updateLetterboxSurface(w)
进入Letterbox的创建和各项设置
//services/core/java/com/android/server/wm/ActivityRecord.java
final LetterboxUiController mLetterboxUiController;
// services/core/java/com/android/server/wm/DisplayContent.java
private final Consumer<WindowState> mApplySurfaceChangesTransaction = w -> {
...
final ActivityRecord activity = w.mActivityRecord;
if (activity != null && activity.isVisibleRequested()) {
// 在这里进入Letterbox的创建和各项设置
activity.updateLetterboxSurface(w);
final boolean updateAllDrawn = activity.updateDrawnWindowStates(w);
if (updateAllDrawn && !mTmpUpdateAllDrawn.contains(activity)) {
mTmpUpdateAllDrawn.add(activity);
}
}
...
}
updateLetterboxSurface
shouldShowLetterboxUi(w)
方法是是否进入LetterBox模式的关键方法
// LetterboxUiController.java
void updateLetterboxSurface(WindowState winHint) {
final WindowState w = mActivityRecord.findMainWindow();
if (w != winHint && winHint != null && w != null) {
return;
}
// 对界面四周需要显示的 Layer 进行位置计算
layoutLetterbox(winHint);
if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) {
// 对 Surface 执行创建、参数设置等操作
mLetterbox.applySurfaceChanges(mActivityRecord.getSyncTransaction());
}
}
void layoutLetterbox(WindowState winHint) {
final WindowState w = mActivityRecord.findMainWindow();
if (w == null || winHint != null && w != winHint) {
return;
}
updateRoundedCorners(w);
updateWallpaperForLetterbox(w);
// 是否进入 Letterbox 模式的关键判断
if (shouldShowLetterboxUi(w)) {
if (mLetterbox == null) {
// 把具体逻辑委托给 Letterbox
mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null),
mActivityRecord.mWmService.mTransactionFactory,
mLetterboxConfiguration::isLetterboxActivityCornersRounded,
this::getLetterboxBackgroundColor,
this::hasWallpaperBackgroudForLetterbox,
this::getLetterboxWallpaperBlurRadius,
this::getLetterboxWallpaperDarkScrimAlpha);
mLetterbox.attachInput(w);
}
mActivityRecord.getPosition(mTmpPoint);
// Get the bounds of the "space-to-fill". The transformed bounds have the highest
// priority because the activity is launched in a rotated environment. In multi-window
// mode, the task-level represents this. In fullscreen-mode, the task container does
// (since the orientation letterbox is also applied to the task).
final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds();
final Rect spaceToFill = transformedBounds != null
? transformedBounds
: mActivityRecord.inMultiWindowMode()
? mActivityRecord.getRootTask().getBounds()
: mActivityRecord.getRootTask().getParent().getBounds();
// 位置计算
mLetterbox.layout(spaceToFill, w.getFrame(), mTmpPoint);
} else if (mLetterbox != null) {
mLetterbox.hide();
}
}
LetterBox.LetterboxSurface
在这里进行LetterBox Surface的创建、设置、显示和隐藏
// services/core/java/com/android/server/wm/Letterbox.java
// LetterBox在上下左右的填充
private final LetterboxSurface mTop = new LetterboxSurface("top");
private final LetterboxSurface mLeft = new LetterboxSurface("left");
private final LetterboxSurface mBottom = new LetterboxSurface("bottom");
private final LetterboxSurface mRight = new LetterboxSurface("right");
// 防止壁纸透过圆角被看到。它不包含在在mSurfaces数组中,是因为它不需要在如notIntersectsOrFullyContains或attachInput的方法中使用
private final LetterboxSurface mBehind = new LetterboxSurface("behind");
private final LetterboxSurface[] mSurfaces = { mLeft, mTop, mRight, mBottom };
public void applySurfaceChanges(SurfaceControl.Transaction t) {
if (!needsApplySurfaceChanges()) {
// Nothing changed.
return;
}
mSurfaceFrameRelative.set(mLayoutFrameRelative);
if (!mSurfaceFrameRelative.isEmpty()) {
if (mSurface == null) {
// 创建挂在 ActivityRecord 节点下的 Surface,设置为 ColorLayer 类型
createSurface(t);
}
// 设置颜色、位置、裁剪
mColor = mColorSupplier.get();
t.setColor(mSurface, getRgbColorArray());
t.setPosition(mSurface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top);
t.setWindowCrop(mSurface, mSurfaceFrameRelative.width(),
mSurfaceFrameRelative.height());
// 对壁纸背景设置透明度和模糊度
mHasWallpaperBackground = mHasWallpaperBackgroundSupplier.get();
updateAlphaAndBlur(t);
t.show(mSurface);
} else if (mSurface != null) {
t.hide(mSurface);
}
if (mSurface != null && mInputInterceptor != null) {
mInputInterceptor.updateTouchableRegion(mSurfaceFrameRelative);
t.setInputWindowInfo(mSurface, mInputInterceptor.mWindowHandle);
}
}
private void createSurface(SurfaceControl.Transaction t) {
mSurface = mSurfaceControlFactory.get()
.setName("Letterbox - " + mType)
.setFlags(HIDDEN)
// 可以从SurfaceFlinger dump信息看到letterbox的图层CompType为SOLID_COLOR
.setColorLayer()
.setCallsite("LetterboxSurface.createSurface")
.build();
t.setLayer(mSurface, -1).setColorSpaceAgnostic(mSurface, true);
}
Google对应用开发者的建议
虽然Android 12增强功能改进了LetterBox应用的外观,但最好的改进是让您的应用可调整大小并为它们提供响应式UI,以适应各种尺寸的屏幕。可调整大小的应用程序支持多窗口模式,响应式UI提供最佳用户体验。