Obsługa trybów składanego wyświetlacza

Urządzenia składane zapewniają wyjątkowe wrażenia podczas oglądania. Tryb tylnego wyświetlacza i tryb dwóch ekranów umożliwiają tworzenie specjalnych funkcji wyświetlania na urządzeniach składanych, takich jak podgląd selfie z tylnego aparatu czy jednoczesne, ale różne wyświetlanie na ekranach wewnętrznym i zewnętrznym.

Tryb wyświetlania z tyłu

Zwykle po otwarciu urządzenia składanego aktywny jest tylko ekran wewnętrzny. Tryb tylnego wyświetlacza umożliwia przeniesienie aktywności na zewnętrzny ekran urządzenia składanego, który zwykle jest odwrócony od użytkownika, gdy urządzenie jest rozłożone. Wewnętrzny wyświetlacz wyłączy się automatycznie.

Nowatorskim zastosowaniem jest wyświetlanie podglądu z aparatu na ekranie zewnętrznym, dzięki czemu użytkownicy mogą robić selfie tylnym aparatem, który zwykle zapewnia znacznie lepszą jakość zdjęć niż aparat przedni.

Aby włączyć tryb tylnego wyświetlacza, użytkownicy muszą odpowiedzieć na pytanie w oknie dialogowym, które pozwala aplikacji przełączać ekrany, np.:

Rysunek 1. Okno systemowe umożliwiające włączenie trybu tylnego wyświetlacza.

System tworzy okno, więc nie musisz niczego programować. W zależności od stanu urządzenia wyświetlają się różne okna dialogowe. Na przykład jeśli urządzenie jest zamknięte, system prosi użytkownika o jego rozłożenie. Nie możesz dostosować okna dialogowego, a na urządzeniach różnych producentów OEM może ono wyglądać inaczej.

Tryb tylnego wyświetlacza możesz wypróbować w aplikacji Aparat na Pixelu Fold. Przykładową implementację znajdziesz w samouczku Optymalizacja aplikacji aparatu na urządzeniach składanych za pomocą Jetpack WindowManager.

Tryb dwóch ekranów

Tryb dwóch ekranów umożliwia wyświetlanie treści na obu ekranach urządzenia składanego jednocześnie. Tryb dwóch ekranów jest dostępny na Pixelu Fold z Androidem 14 (API na poziomie 34) lub nowszym.

Przykładem zastosowania jest tłumacz na 2 ekranach.

Rysunek 2. Tłumacz na urządzeniu z dwoma ekranami, na których wyświetlają się różne treści.

Włączanie trybów za pomocą kodu

Tryb tylnego wyświetlacza i tryb dwóch ekranów są dostępne w interfejsach API Jetpack WindowManager od wersji biblioteki 1.2.0-beta03.

Dodaj zależność WindowManager do pliku build.gradle modułu aplikacji:

Groovy

dependencies {
    // TODO: Define window_version in your project's build configuration.
    implementation "androidx.window:window:$window_version"
}

Kotlin

dependencies {
    // Define window_version in your project's build configuration.
    implementation("androidx.window:window:$window_version")
}

Punktem wejścia jest WindowAreaController, który zawiera informacje i zachowania związane z przenoszeniem okien między wyświetlaczami lub między obszarami wyświetlania na urządzeniu. WindowAreaController umożliwia wysyłanie zapytań dotyczących listy dostępnych obiektów WindowAreaInfo.

Użyj WindowAreaInfo, aby uzyskać dostęp do WindowAreaSession, interfejsu, który reprezentuje funkcję obszaru aktywnego okna. Użyj WindowAreaSession, aby określić dostępność konkretnego WindowAreaCapability.

Każda funkcja jest powiązana z określonym WindowAreaCapability.Operation. W wersji 1.2.0-beta03 biblioteka Jetpack WindowManager obsługuje 2 rodzaje operacji:

Oto przykład deklarowania zmiennych dla trybu tylnego wyświetlacza i trybu dwóch ekranów w głównym działaniu aplikacji:

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;

Aby zainicjować zmienne w metodzie onCreate() aktywności:

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;
        }
    }
});

Przed rozpoczęciem operacji sprawdź dostępność danej funkcji:

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.
}

Tryb dwóch ekranów

W tym przykładzie sesja zostanie zamknięta, jeśli funkcja jest już aktywna. W przeciwnym razie zostanie wywołana funkcja 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);
    }
}

Zwróć uwagę na użycie głównej aktywności aplikacji jako argumentu WindowAreaPresentationSessionCallback.

Interfejs API korzysta z podejścia opartego na odbiorniku: gdy wysyłasz żądanie wyświetlenia treści na drugim ekranie urządzenia składanego, inicjujesz sesję, która jest zwracana przez metodę onSessionStarted() odbiornika. Po zamknięciu sesji otrzymasz potwierdzenie w metodzie onSessionEnded().

Aby utworzyć odbiorcę, zaimplementuj interfejs WindowAreaPresentationSessionCallback:

Kotlin

class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback

Java

public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback

Detektor musi implementować metody onSessionStarted(), onSessionEnded(),onContainerVisibilityChanged(). Metody wywołania zwrotnego informują Cię o stanie sesji i umożliwiają odpowiednie zaktualizowanie aplikacji.

Wywołanie zwrotne onSessionStarted() otrzymuje jako argument wartość WindowAreaSessionPresenter. Argumentem jest kontener, który umożliwia dostęp do obszaru okna i wyświetlanie treści. Prezentacja może zostać automatycznie zamknięta przez system, gdy użytkownik opuści główne okno aplikacji, lub może zostać zamknięta przez wywołanie funkcji WindowAreaSessionPresenter#close().

W przypadku pozostałych wywołań zwrotnych dla uproszczenia sprawdź w treści funkcji, czy nie występują błędy, i zarejestruj stan:

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);
}

Aby zachować spójność w ekosystemie, używaj oficjalnej ikony Dual Screen, aby informować użytkowników, jak włączyć lub wyłączyć tryb dwóch ekranów.

Działający przykład znajdziesz w pliku DualScreenActivity.kt.

Tryb wyświetlania z tyłu

Podobnie jak w przykładzie trybu dwuekranowego, poniższy przykład funkcji zamyka sesję, jeśli funkcja jest już aktywna, lub w przeciwnym razie wywołuje funkcję transferActivityToWindowArea():toggleRearDisplayMode()

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 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);
    }
}

W tym przypadku wyświetlana aktywność jest używana jako WindowAreaSessionCallback, co jest prostsze do wdrożenia, ponieważ wywołanie zwrotne nie otrzymuje prezentera, który umożliwia wyświetlanie treści w obszarze okna, ale zamiast tego przenosi całą aktywność do innego obszaru:

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}");
    }
}

Aby zachować spójność w ekosystemie, używaj oficjalnej ikony tylnego aparatu, aby informować użytkowników, jak włączyć lub wyłączyć tryb tylnego wyświetlacza.

Dodatkowe materiały