Складные устройства предлагают уникальные возможности просмотра. Режим отображения на задней панели и режим двух экранов позволяют создавать специальные функции отображения для складных устройств, такие как предварительный просмотр селфи с задней камеры и одновременное отображение информации на внутреннем и внешнем экранах, но с разными параметрами.
Режим заднего дисплея
Обычно при разворачивании складного устройства активен только внутренний экран. Режим отображения на задней панели позволяет перенести действие на внешний экран складного устройства, который обычно отвернут от пользователя, когда устройство разложено. Внутренний дисплей автоматически отключается.
Новым приложением является отображение предварительного просмотра камеры на внешнем экране, что позволяет пользователям делать селфи с помощью задней камеры, которая обычно обеспечивает гораздо лучшее качество фотосъемки, чем фронтальная камера.
Чтобы активировать режим заднего дисплея, пользователи отвечают на диалоговое окно, разрешая приложению переключать экраны, например:

Система создаёт диалоговое окно, поэтому вам не требуется никакой разработки. В зависимости от состояния устройства появляются различные диалоговые окна; например, система предлагает пользователю развернуть устройство, если оно закрыто. Настроить диалоговое окно невозможно, и оно может различаться на устройствах разных производителей.
Вы можете попробовать режим отображения на задней панели с помощью приложения камеры Pixel Fold. Пример реализации см. в практической работе «Оптимизация приложения камеры на складных устройствах с помощью Jetpack WindowManager» .
Режим двух экранов
Режим двух экранов позволяет отображать контент на обоих дисплеях складного устройства одновременно. Режим двух экранов доступен на Pixel Fold под управлением Android 14 (API уровня 34) и выше.
Примером использования является двухэкранный переводчик.

Включить режимы программно
Доступ к режиму заднего дисплея и режиму двух экранов можно получить через API Jetpack WindowManager , начиная с версии библиотеки 1.2.0-beta03.
Добавьте зависимость WindowManager в файл build.gradle модуля вашего приложения:
Круто
dependencies {
// TODO: Define window_version in your project's build configuration.
implementation "androidx.window:window:$window_version"
}
Котлин
dependencies {
// Define window_version in your project's build configuration.
implementation("androidx.window:window:$window_version")
}
Точкой входа является WindowAreaController , который предоставляет информацию и поведение, связанные с перемещением окон между дисплеями или между областями отображения на устройстве. WindowAreaController позволяет запрашивать список доступных объектов WindowAreaInfo .
Используйте WindowAreaInfo для доступа к WindowAreaSession — интерфейсу, представляющему активную функцию области окна. WindowAreaSession используется для определения доступности конкретной возможности WindowAreaCapability .
Каждая возможность связана с определённой WindowAreaCapability.Operation . В версии 1.2.0-beta03 Jetpack WindowManager поддерживает два вида операций:
-
WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA, который используется для запуска режима двух экранов -
WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA, которая используется для запуска режима заднего дисплея
Вот пример того, как объявить переменные для режима заднего дисплея и режима двух экранов в основной деятельности вашего приложения:
Котлин
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
Ява
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;
Вот как инициализировать переменные в методе onCreate() вашей активности:
Котлин
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
}
}
}
Ява
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;
}
}
});
Перед началом операции проверьте наличие конкретной возможности:
Котлин
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.
}
}
Ява
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.
}
Режим двух экранов
В следующем примере сеанс закрывается, если возможность уже активна, или в противном случае вызывается функция presentContentOnWindowArea() :
Котлин
fun toggleDualScreenMode() {
if (windowAreaSession != null) {
windowAreaSession?.close()
}
else {
windowAreaInfo?.token?.let { token ->
windowAreaController.presentContentOnWindowArea(
token = token,
activity = this,
executor = displayExecutor,
windowAreaPresentationSessionCallback = this
)
}
}
}
Ява
private void toggleDualScreenMode() {
if(windowAreaSession != null) {
windowAreaSession.close();
}
else {
Binder token = windowAreaInfo.getToken();
windowAreaController.presentContentOnWindowArea( token, this, displayExecutor, this);
}
}
Обратите внимание на использование основного действия приложения в качестве аргумента WindowAreaPresentationSessionCallback .
API использует подход, основанный на прослушивании: когда вы отправляете запрос на отображение содержимого на другом дисплее складного элемента, вы инициируете сеанс, который возвращается через метод onSessionStarted() прослушивателя. После закрытия сеанса вы получаете подтверждение в методе onSessionEnded() .
Чтобы создать прослушиватель, реализуйте интерфейс WindowAreaPresentationSessionCallback :
Котлин
class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback
Ява
public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback
Слушатель должен реализовать методы onSessionStarted() , onSessionEnded(), и onContainerVisibilityChanged() . Методы обратного вызова уведомляют вас о состоянии сеанса и позволяют обновлять приложение соответствующим образом.
Обратный вызов onSessionStarted() получает в качестве аргумента WindowAreaSessionPresenter . Аргумент представляет собой контейнер, позволяющий получить доступ к области окна и отобразить содержимое. Система может автоматически закрыть презентацию, когда пользователь покидает основное окно приложения, или закрыть презентацию вызовом WindowAreaSessionPresenter#close() .
Для простоты в остальных обратных вызовах просто проверяйте тело функции на наличие ошибок и регистрируйте состояние:
Котлин
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")
}
Ява
@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);
}
Чтобы обеспечить единообразие в экосистеме, используйте официальный значок Dual Screen, чтобы указать пользователям, как включить или отключить режим двух экранов.
Рабочий пример см. в DualScreenActivity.kt .
Режим заднего дисплея
Аналогично примеру с режимом двух экранов, следующий пример функции toggleRearDisplayMode() закрывает сеанс, если возможность уже активна, или в противном случае вызывает функцию transferActivityToWindowArea() :
Котлин
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
)
}
}
}
Ява
void toggleRearDisplayMode() {
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);
}
}
В этом случае отображаемая активность используется как WindowAreaSessionCallback , что проще реализовать, поскольку обратный вызов не получает презентер, позволяющий отображать содержимое в области окна, а вместо этого переносит всю активность в другую область:
Котлин
override fun onSessionStarted() {
Log.d(logTag, "onSessionStarted")
}
override fun onSessionEnded(t: Throwable?) {
if(t != null) {
Log.e(logTag, "Something was broken: ${t.message}")
}
}
Ява
@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}");
}
}
Чтобы обеспечить согласованность в экосистеме, используйте официальный значок задней камеры , чтобы указать пользователям, как включить или отключить режим заднего дисплея.
Дополнительные ресурсы
- Оптимизируйте приложение камеры на складных устройствах с помощью лабораторной работы Jetpack WindowManager
- сводка пакета
androidx.window.area - Пример кода Jetpack WindowManager: