Android之LetterBox介绍

3,001 阅读5分钟

什么是LetterBox

应用可以配置保持固定的大小或方向,而不考虑显示大小或设备方向。如果一个应用请求固定的方向或是不可调整大小的,并且其最大或最小比例与设备的显示比例不兼容,应用就会以LetterBox模式打开。

LetterBox通常出现在大屏幕设备上,尤其是可折叠设备上,因为设备的显示尺寸和宽高比通常与标准手机不同,而大多数应用程序都是为标准手机设计的。

哪些情况会触发LetterBox显示

当应用程序无法调整大小或它们具有固定方向时,就可能会发生Letterboxing。控制应用程序方向和可调整性的配置包括下面这些:

  1. screenOrientation: 为应用程序指定固定的方向,也可以在运行时使用Activity#setRequestedOrientation()方法设置
  2. resizeableActivity:当应用声明不可resize并且声明的宽高比与容器不兼容的时候(如屏幕宽高超出android:maxAspectRatio、minAspectRatio)
  3. setIgnoreOrientationRequest(true) 这是系统设置忽略屏幕方向后,以横屏模式下打开一个强制竖屏的界面

可以使用adb命令验证: adb shell wm set-ignore-orientation-request true

LetterBox模式打开微信效果演示

Weixin_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#3Letterbox - right#3就是微信左右两边填充的黑色区域。

Android 12/12L对LetterBox的美化增强

Google文档

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提供最佳用户体验。

Android 12 Enhanced letterboxing