App faltbar erkennen

Große aufgeklappte Displays und einzigartige gefaltete Zustände ermöglichen neue Nutzungsmöglichkeiten auf faltbaren Geräten. Wenn Sie Ihre App für faltbare Geräte optimieren möchten, verwenden Sie die Jetpack WindowManager-Bibliothek, die eine API-Oberfläche für Fensterfunktionen faltbarer Geräte wie Faltungen und Scharniere bietet. Wenn Ihre App faltbar ist, kann sie ihr Layout anpassen, um wichtige Inhalte nicht im Bereich von Faltungen oder Scharnieren zu platzieren und Faltungen und Scharniere als natürliche Trennzeichen zu verwenden.

Wenn Sie wissen, ob ein Gerät Konfigurationen wie die Tisch- oder Buchposition unterstützt, können Sie Entscheidungen zur Unterstützung verschiedener Layouts oder zur Bereitstellung bestimmter Funktionen treffen.

Fensterinformationen

Die WindowInfoTracker-Schnittstelle in Jetpack WindowManager macht Informationen zum Fensterlayout verfügbar. Die Methode windowLayoutInfo() der Schnittstelle gibt einen Stream von WindowLayoutInfo-Daten zurück, die Ihre App über den Faltstatus eines faltbaren Geräts informieren. Mit der Methode WindowInfoTracker#getOrCreate() wird eine Instanz von WindowInfoTracker erstellt.

WindowManager bietet Unterstützung für die Erfassung von WindowLayoutInfo-Daten mit Kotlin-Flows und Java-Callbacks.

Kotlin-Datenflüsse

Zum Starten und Beenden der WindowLayoutInfo-Datenerhebung können Sie eine lebenszyklusbezogene Coroutine, die neu gestartet werden kann, verwenden. Der repeatOnLifecycle-Codeblock wird ausgeführt, wenn der Lebenszyklus mindestens STARTED ist, und beendet, wenn der Lebenszyklus STOPPED ist. Die Ausführung des Codeblocks wird automatisch neu gestartet, wenn der Lebenszyklus wieder STARTED ist. Im folgenden Beispiel werden im Codeblock WindowLayoutInfo-Daten erhoben und verwendet:

class DisplayFeaturesActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDisplayFeaturesBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
                    .windowLayoutInfo(this@DisplayFeaturesActivity)
                    .collect { newLayoutInfo ->
                        // Use newLayoutInfo to update the layout.
                    }
            }
        }
    }
}

Java-Rückrufe

Mit der in der androidx.window:window-java-Abhängigkeit enthaltenen Callback-Kompatibilitätsebene können Sie WindowLayoutInfo-Updates ohne Kotlin-Flow erfassen. Das Artefakt enthält die Klasse WindowInfoTrackerCallbackAdapter, die einen WindowInfoTracker anpasst, um das Registrieren (und Aufheben der Registrierung) von Callbacks zum Empfangen von WindowLayoutInfo-Updates zu unterstützen, z. B.:

public class SplitLayoutActivity extends AppCompatActivity {

    private WindowInfoTrackerCallbackAdapter windowInfoTracker;
    private ActivitySplitLayoutBinding binding;
    private final LayoutStateChangeCallback layoutStateChangeCallback =
            new LayoutStateChangeCallback();

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

       windowInfoTracker =
                new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
   }

   @Override
   protected void onStart() {
       super.onStart();
       windowInfoTracker.addWindowLayoutInfoListener(
                this, Runnable::run, layoutStateChangeCallback);
   }

   @Override
   protected void onStop() {
       super.onStop();
       windowInfoTracker
           .removeWindowLayoutInfoListener(layoutStateChangeCallback);
   }

   class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       @Override
       public void accept(WindowLayoutInfo newLayoutInfo) {
           SplitLayoutActivity.this.runOnUiThread( () -> {
               // Use newLayoutInfo to update the layout.
           });
       }
   }
}

RxJava-Unterstützung

Wenn Sie bereits RxJava (Version 2 oder 3) verwenden, können Sie Artefakte nutzen, mit denen Sie ein Observable oder Flowable verwenden können, um WindowLayoutInfo-Updates zu erfassen, ohne einen Kotlin-Flow zu verwenden.

Die von den Abhängigkeiten androidx.window:window-rxjava2 und androidx.window:window-rxjava3 bereitgestellte Kompatibilitätsebene umfasst die Methoden WindowInfoTracker#windowLayoutInfoFlowable() und WindowInfoTracker#windowLayoutInfoObservable(), mit denen Ihre App WindowLayoutInfo-Updates empfangen kann. Beispiel:

class RxActivity: AppCompatActivity {

    private lateinit var binding: ActivityRxBinding

    private var disposable: Disposable? = null
    private lateinit var observable: Observable<WindowLayoutInfo>

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

        // Create a new observable.
        observable = WindowInfoTracker.getOrCreate(this@RxActivity)
            .windowLayoutInfoObservable(this@RxActivity)
   }

   @Override
   protected void onStart() {
       super.onStart();

        // Subscribe to receive WindowLayoutInfo updates.
        disposable?.dispose()
        disposable = observable
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { newLayoutInfo ->
            // Use newLayoutInfo to update the layout.
        }
   }

   @Override
   protected void onStop() {
       super.onStop();

        // Dispose of the WindowLayoutInfo observable.
        disposable?.dispose()
   }
}

Funktionen von faltbaren Displays

Die WindowLayoutInfo-Klasse von Jetpack WindowManager macht die Funktionen eines Anzeigefensters als Liste von DisplayFeature-Elementen verfügbar.

Ein FoldingFeature ist ein Typ von DisplayFeature, der Informationen zu faltbaren Displays enthält, einschließlich der folgenden Eigenschaften:

  • state: Der gefaltete Zustand des Geräts, FLAT oder HALF_OPENED

  • orientation: Die Ausrichtung der Falte oder des Scharniers, HORIZONTAL oder VERTICAL

  • occlusionType: Gibt an, ob der Falz oder das Scharnier einen Teil des Displays verdeckt. Mögliche Werte sind NONE oder FULL.

  • isSeparating: Gibt an, ob durch das Falten oder das Scharnier zwei logische Displaybereiche entstehen (true oder false).

Bei einem faltbaren Gerät, das HALF_OPENED ist, wird immer isSeparating als „true“ gemeldet, da das Display in zwei Anzeigebereiche unterteilt ist. Außerdem ist isSeparating auf einem Gerät mit zwei Displays immer „true“, wenn die Anwendung beide Displays umfasst.

Die FoldingFeature-Eigenschaft bounds (die von DisplayFeature übernommen wird) stellt das umgebende Rechteck eines Falt-Features wie einer Falte oder eines Scharniers dar. Mit den Grenzen können Elemente auf dem Bildschirm relativ zum Feature positioniert werden:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from WindowInfoTracker when the lifecycle is
            // STARTED and stops collection when the lifecycle is STOPPED.
            WindowInfoTracker.getOrCreate(this@MainActivity)
                .windowLayoutInfo(this@MainActivity)
                .collect { layoutInfo ->
                    // New posture information.
                    val foldingFeature = layoutInfo.displayFeatures
                        .filterIsInstance<FoldingFeature>()
                        .firstOrNull()
                    // Use information from the foldingFeature object.
                }
        }
    }
}

Java

private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private final LayoutStateChangeCallback layoutStateChangeCallback =
                new LayoutStateChangeCallback();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    // ...
    windowInfoTracker =
            new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}

@Override
protected void onStart() {
    super.onStart();
    windowInfoTracker.addWindowLayoutInfoListener(
            this, Runnable::run, layoutStateChangeCallback);
}

@Override
protected void onStop() {
    super.onStop();
    windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}

class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
    @Override
    public void accept(WindowLayoutInfo newLayoutInfo) {
        // Use newLayoutInfo to update the Layout.
        List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
        for (DisplayFeature feature : displayFeatures) {
            if (feature instanceof FoldingFeature) {
                // Use information from the feature object.
            }
        }
    }
}

Tischposition

Anhand der Informationen im FoldingFeature-Objekt kann Ihre App Positionen wie „Auf dem Tisch“ unterstützen. Dabei liegt das Smartphone auf einer Oberfläche, das Scharnier befindet sich in einer horizontalen Position und das faltbare Display ist halb geöffnet.

Im Modus „Auf dem Tisch“ können Nutzer ihr Smartphone bedienen, ohne es in der Hand halten zu müssen. Die Position „Auf dem Tisch“ eignet sich hervorragend zum Ansehen von Medien, zum Fotografieren und für Videoanrufe.

Abbildung 1. Eine Videoplayer-App im Modus „Auf dem Tisch“ – Video auf dem vertikalen Teil des Bildschirms, Wiedergabesteuerelemente auf dem horizontalen Teil.

Verwende FoldingFeature.State und FoldingFeature.Orientation, um festzustellen, ob sich das Gerät in der Tischaufstellung befindet:

Kotlin

fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}

Java

boolean isTableTopPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

Sobald Sie wissen, dass sich das Gerät in der Tischposition befindet, passen Sie das App-Layout entsprechend an. Bei Media-Apps bedeutet das in der Regel, dass die Wiedergabe über dem Falz platziert wird und Steuerelemente und zusätzliche Inhalte direkt danach folgen, um eine freihändige Wiedergabe zu ermöglichen.

Unter Android 15 (API-Level 35) und höher können Sie eine synchrone API aufrufen, um zu erkennen, ob ein Gerät den Tischmodus unterstützt, unabhängig vom aktuellen Status des Geräts.

Die API gibt eine Liste der vom Gerät unterstützten Körperhaltungen zurück. Wenn die Liste die Tischaufstellung enthält, können Sie das App-Layout aufteilen, um die Aufstellung zu unterstützen, und A/B-Tests für die App-Benutzeroberfläche für Tisch- und Vollbildlayouts ausführen.

Kotlin

if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
    val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
    if (postures.contains(TABLE_TOP)) {
        // Device supports tabletop posture.
   }
}

Java

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
    List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures();
    if (postures.contains(SupportedPosture.TABLETOP)) {
        // Device supports tabletop posture.
    }
}

Beispiele

Buchstatus

Eine weitere einzigartige Funktion für faltbare Geräte ist die Buchposition, bei der das Gerät halb geöffnet und das Scharnier vertikal ausgerichtet ist. Die Buchposition eignet sich hervorragend zum Lesen von E‑Books. Mit dem zweiseitigen Layout auf einem großen faltbaren Display, das wie ein gebundenes Buch geöffnet wird, wird die Erfahrung des Lesens eines echten Buchs nachempfunden.

Sie kann auch für die Fotografie verwendet werden, wenn Sie freihändig Fotos mit einem anderen Seitenverhältnis aufnehmen möchten.

Implementieren Sie die Buchhaltung mit denselben Techniken, die für die Tischhaltung verwendet werden. Der einzige Unterschied besteht darin, dass der Code prüfen sollte, ob die Ausrichtung des faltbaren Displays vertikal statt horizontal ist:

Kotlin

fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}

Java

boolean isBookPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

Änderungen der Fenstergröße

Der Anzeigebereich einer App kann sich aufgrund einer Änderung der Gerätekonfiguration ändern, z. B. wenn das Gerät auf- oder zugeklappt oder gedreht wird oder wenn die Größe eines Fensters im Multi-Window-Modus geändert wird.

Mit der Jetpack WindowManager-Klasse WindowMetricsCalculator können Sie die aktuellen und maximalen Fenstermesswerte abrufen. Wie die Plattform WindowMetrics, die in API-Level 30 eingeführt wurde, stellt auch WindowManager WindowMetrics die Fenstergrenzen bereit. Die API ist jedoch abwärtskompatibel bis API-Level 14.

Weitere Informationen finden Sie unter Klassen für Fenstergrößen verwenden.

Zusätzliche Ressourcen

Produktproben

  • Jetpack WindowManager: Beispiel für die Verwendung der Jetpack WindowManager-Bibliothek
  • Jetcaster : Implementierung der Tischposition mit Compose

Codelabs