Поддержка режимов складного дисплея

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

Режим заднего дисплея

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

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

Чтобы активировать режим заднего дисплея, пользователи отвечают на диалоговое окно, позволяющее приложению переключать экраны, например:

Рисунок 1. Системное диалоговое окно, позволяющее запустить режим заднего дисплея.

Система создает диалог, поэтому никаких доработок с вашей стороны не требуется. В зависимости от состояния устройства появляются разные диалоговые окна; например, система предлагает пользователям развернуть устройство, если оно закрыто. Диалоговое окно нельзя настроить, но оно может различаться на устройствах разных производителей.

Вы можете попробовать режим заднего дисплея с помощью приложения камеры Pixel Fold. Ознакомьтесь с примером реализации в лаборатории кода. Раскройте возможности камеры .

Режим двух экранов

Режим двух экранов позволяет отображать контент на обоих дисплеях складного устройства одновременно. Режим двух экранов доступен на устройствах Pixel Fold под управлением Android 14 (уровень API 34) или выше.

Примером использования является переводчик с двумя экранами.

Рис. 2. Двухэкранный переводчик, показывающий разное содержимое на переднем и заднем дисплеях.

Включить режимы программно

Вы можете получить доступ к режиму заднего дисплея и режиму двух экранов через API-интерфейсы Jetpack WindowManager , начиная с версии библиотеки 1.2.0-beta03.

Добавьте зависимость WindowManager в файл build.gradle модуля вашего приложения:

классный

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

Котлин

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

Точкой входа является WindowAreaController , который предоставляет информацию и поведение, связанные с перемещением окон между дисплеями или между областями отображения на устройстве. WindowAreaController позволяет запрашивать список доступных объектов WindowAreaInfo .

Используйте WindowAreaInfo для доступа к WindowAreaSession — интерфейсу, который представляет функцию активной области окна. Используйте WindowAreaSession чтобы определить доступность определенного WindowAreaCapability .

Каждая возможность связана с конкретной WindowAreaCapability.Operation . В версии 1.2.0-beta03 Jetpack WindowManager поддерживает два типа операций:

Вот пример того, как объявить переменные для режима заднего дисплея и режима двух экранов в основной активности вашего приложения:

Котлин

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 currently available to be enabled.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
      // The selected display mode is currently available to 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 currently available to be enabled.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE)) {
  // The selected display mode is currently available to 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);
}

Чтобы обеспечить согласованность всей экосистемы, используйте официальный значок двойного экрана, чтобы указать пользователям, как включить или отключить режим двойного экрана.

Рабочий образец можно найти на 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 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);
    }
}

В этом случае отображаемое действие используется как 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}");
    }
}

Чтобы обеспечить согласованность всей экосистемы, используйте официальный значок задней камеры, чтобы указать пользователям, как включить или отключить режим заднего дисплея.

Дополнительные ресурсы

{% дословно %} {% дословно %} {% дословно %} {% дословно %}