支持可折叠显示屏模式

可折叠设备可提供独特的观看体验。借助后置显示屏模式和双屏幕模式,您可以为可折叠设备构建特殊的显示功能,例如后置摄像头自拍预览以及内外屏上同步但不同的显示屏。

后置显示屏模式

通常,当可折叠设备展开时,只有内屏处于活动状态,外屏通常背向用户。借助后置显示屏模式,您可以将 activity 移至可折叠设备的外屏。内屏会自动关闭。

一种新颖的应用会在外屏上显示相机预览,以便用户可以使用后置摄像头自拍,出来的拍照效果通常要比前置摄像头好很多。

如需启用后置显示屏模式,用户需响应对话框以允许应用切换屏幕,例如:

图 1. 允许启动后置显示屏模式的系统对话框。

对话框由系统创建,因此您无需进行任何开发。根据设备状态显示不同的对话框;例如,如果设备处于合上状态,系统会引导用户展开设备。您无法自定义该对话框,并且该对话框可能会因不同 OEM 的设备而异。

您可以使用 Pixel Fold 相机应用试用后置显示屏模式。如需查看示例实现,请参阅使用 Jetpack WindowManager 在可折叠设备上优化相机应用 Codelab。

双屏幕模式

借助双屏幕模式,您可以同时在可折叠设备的两个显示屏上显示内容。双屏模式适用于搭载 Android 14(API 级别 34)或更高版本的 Pixel Fold。

双屏幕模式的一个用例示例是双屏幕口译模式。

图 2. 在前后显示屏上显示不同内容的双屏幕口译模式。

以编程方式启用模式

从库版本 1.2.0-beta03 开始,您可以通过 Jetpack WindowManager API 使用后置显示屏模式和双屏幕模式。

将 WindowManager 依赖项添加到应用的模块 build.gradle 文件中:

Groovy

dependencies {
    implementation "androidx.window:window:1.2.0-beta03"
}

Kotlin

dependencies {
    implementation("androidx.window:window:1.2.0-beta03")
}

入口点是 WindowAreaController,它提供与在设备显示屏之间或显示区域之间移动窗口有关的信息和行为。借助 WindowAreaController,您可以查询可用 WindowAreaInfo 对象的列表。

使用 WindowAreaInfo 访问 WindowAreaSession,该接口表示处于活动状态的窗口区域功能。使用 WindowAreaSession 确定特定 WindowAreaCapability 的可用性。

每个 capability 都与特定的 WindowAreaCapability.Operation 相关。在版本 1.2.0-beta03 中,Jetpack WindowManager 支持两种操作:

以下示例展示了如何在应用的主 activity 中声明后置显示屏模式和双屏幕模式的变量:

Kotlin

private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var windowAreaSession: WindowAreaSession? = null
private var windowAreaInfo: WindowAreaInfo? = null
private var capabilityStatus: WindowAreaCapability.Status =
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED

private val dualScreenOperation = WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA

Java

private WindowAreaControllerCallbackAdapter windowAreaController = null;
private Executor displayExecutor = null;
private WindowAreaSessionPresenter windowAreaSession = null;
private WindowAreaInfo windowAreaInfo = null;
private WindowAreaCapability.Status capabilityStatus  =
        WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED;

private WindowAreaCapability.Operation dualScreenOperation =
        WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA;
private WindowAreaCapability.Operation rearDisplayOperation =
        WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA;

下面介绍了如何在 activity 的 onCreate() 方法中初始化变量:

Kotlin

displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaController.getOrCreate()

lifecycleScope.launch(Dispatchers.Main) {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        windowAreaController.windowAreaInfos
            .map { info -> info.firstOrNull { it.type == WindowAreaInfo.Type.TYPE_REAR_FACING } }
            .onEach { info -> windowAreaInfo = info }
            .map { it?.getCapability(operation)?.status ?: WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
            .distinctUntilChanged()
            .collect {
                capabilityStatus = it
            }
    }
}

Java

displayExecutor = ContextCompat.getMainExecutor(this);
windowAreaController = new WindowAreaControllerCallbackAdapter(WindowAreaController.getOrCreate());
windowAreaController.addWindowAreaInfoListListener(displayExecutor, this);

windowAreaController.addWindowAreaInfoListListener(displayExecutor,
  windowAreaInfos -> {
    for(WindowAreaInfo newInfo : windowAreaInfos){
        if(newInfo.getType().equals(WindowAreaInfo.Type.TYPE_REAR_FACING)){
            windowAreaInfo = newInfo;
            capabilityStatus = newInfo.getCapability(presentOperation).getStatus();
            break;
        }
    }
});

在开始操作之前,请检查特定功能的可用性:

Kotlin

when (capabilityStatus) {
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
      // The selected display mode is not supported on this device.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
      // The selected display mode is not available.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
      // The selected display mode is available and can be enabled.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
      // The selected display mode is already active.
    }
    else -> {
      // The selected display mode status is unknown.
    }
}

Java

if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED)) {
  // The selected display mode is not supported on this device.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE)) {
  // The selected display mode is not available.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE)) {
  // The selected display mode is available and can be enabled.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE)) {
  // The selected display mode is already active.
}
else {
  // The selected display mode status is unknown.
}

双屏幕模式

以下示例会在 capability 已处于活动状态时关闭会话,否则会调用 presentContentOnWindowArea() 函数:

Kotlin

fun toggleDualScreenMode() {
    if (windowAreaSession != null) {
        windowAreaSession?.close()
    }
    else {
        windowAreaInfo?.token?.let { token ->
            windowAreaController.presentContentOnWindowArea(
                token = token,
                activity = this,
                executor = displayExecutor,
                windowAreaPresentationSessionCallback = this
            )
        }
    }
}

Java

private void toggleDualScreenMode() {
    if(windowAreaSession != null) {
        windowAreaSession.close();
    }
    else {
        Binder token = windowAreaInfo.getToken();
        windowAreaController.presentContentOnWindowArea( token, this, displayExecutor, this);
    }
}

请注意,该应用的主 activity 用作 WindowAreaPresentationSessionCallback 参数。

该 API 使用监听器方法:当您发出向可折叠设备的其他显示屏呈现内容的请求时,您将启动一个通过监听器的 onSessionStarted() 方法返回的会话。关闭该会话时,您会在 onSessionEnded() 方法中收到确认消息。

如需创建监听器,请实现 WindowAreaPresentationSessionCallback 接口:

Kotlin

class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback

Java

public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback

监听器需要实现 onSessionStarted()onSessionEnded(),onContainerVisibilityChanged() 方法。回调方法会通知您会话状态,并让您能够相应地更新应用。

onSessionStarted() 回调会接收 WindowAreaSessionPresenter 作为参数。该参数是可让您访问窗口区域并显示内容的容器。展示内容可以在用户离开应用主窗口时由系统自动关闭,也可以通过调用 WindowAreaSessionPresenter#close() 来关闭。

对于其他回调,为简单起见,只需检查函数主体是否有任何错误并记录状态即可:

Kotlin

override fun onSessionStarted(session: WindowAreaSessionPresenter) {
    windowAreaSession = session
    val view = TextView(session.context)
    view.text = "Hello world!"
    session.setContentView(view)
}

override fun onSessionEnded(t: Throwable?) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}")
    }
}

override fun onContainerVisibilityChanged(isVisible: Boolean) {
    Log.d(logTag, "onContainerVisibilityChanged. isVisible = $isVisible")
}

Java

@Override
public void onSessionStarted(@NonNull WindowAreaSessionPresenter session) {
    windowAreaSession = session;
    TextView view = new TextView(session.getContext());
    view.setText("Hello world, from the other screen!");
    session.setContentView(view);
}

@Override public void onSessionEnded(@Nullable Throwable t) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}");
    }
}

@Override public void onContainerVisibilityChanged(boolean isVisible) {
    Log.d(logTag, "onContainerVisibilityChanged. isVisible = " + isVisible);
}

为了在整个生态系统中保持一致,请使用双屏幕官方图标向用户指明如何启用或停用双屏幕模式。

如需查看可运行的示例,请参阅 DualScreenActivity.kt

后置显示屏模式

与双屏幕模式示例类似,toggleRearDisplayMode() 函数的以下示例会在 capability 已处于活动状态时关闭会话,否则会调用 transferActivityToWindowArea() 函数:

Kotlin

fun toggleRearDisplayMode() {
    if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
        if(windowAreaSession == null) {
            windowAreaSession = windowAreaInfo?.getActiveSession(
                operation
            )
        }
        windowAreaSession?.close()
    } else {
        windowAreaInfo?.token?.let { token ->
            windowAreaController.transferActivityToWindowArea(
                token = token,
                activity = this,
                executor = displayExecutor,
                windowAreaSessionCallback = this
            )
        }
    }
}

Java

void toggleDualScreenMode() {
    if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
        if(windowAreaSession == null) {
            windowAreaSession = windowAreaInfo.getActiveSession(
                operation
            )
        }
        windowAreaSession.close()
    }
    else {
        Binder token = windowAreaInfo.getToken();
        windowAreaController.transferActivityToWindowArea(token, this, displayExecutor, this);
    }
}

在这种情况下,显示的 activity 用作 WindowAreaSessionCallback,实现起来更简单,因为回调不会接收允许在窗口区域显示内容的 Presenter,而是会将整个 activity 转移到另一个区域:

Kotlin

override fun onSessionStarted() {
    Log.d(logTag, "onSessionStarted")
}

override fun onSessionEnded(t: Throwable?) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}")
    }
}

Java

@Override public void onSessionStarted(){
    Log.d(logTag, "onSessionStarted");
}

@Override public void onSessionEnded(@Nullable Throwable t) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}");
    }
}

为了在整个生态系统中保持一致,请使用后置摄像头官方图标来向用户指明如何启用或停用后置显示屏模式。

其他资源