Ein Layout mit zwei Bereichen erstellen

Schreiben Sie jetzt
Jetpack Compose ist das empfohlene UI-Toolkit für Android. Hier erfahren Sie, wie Sie in „Compose“ mit Layouts arbeiten.

Jeder Bildschirm in Ihrer App muss responsiv sein und sich an den verfügbaren Platz anpassen. Sie können eine responsive Benutzeroberfläche mit ConstraintLayout erstellen, die einen Ansatz mit einem einzelnen Fenster auf viele Größen skalieren lässt. Größere Geräte können jedoch von einer Aufteilung des Layouts in mehrere Bereiche profitieren. Beispielsweise möchten Sie vielleicht, dass auf einem Bildschirm neben einer Liste mit Details zum ausgewählten Element eine Liste von Elementen angezeigt wird.

Die Komponente SlidingPaneLayout unterstützt die parallele Darstellung von zwei Bereichen auf größeren Geräten und faltbaren Displays. Auf kleineren Geräten wie Smartphones wird automatisch jeweils nur ein Fenster angezeigt.

Eine gerätespezifische Anleitung finden Sie in der Übersicht zur Bildschirmkompatibilität.

Einrichten

Wenn Sie SlidingPaneLayout verwenden möchten, fügen Sie die folgende Abhängigkeit in die Datei build.gradle Ihrer Anwendung ein:

Groovig

dependencies {
    implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
}

Kotlin

dependencies {
    implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
}

Konfiguration des XML-Layouts

SlidingPaneLayout bietet ein horizontales Layout mit zwei Bereichen zur Verwendung auf der obersten Ebene einer UI. Bei diesem Layout wird der erste Bereich als Inhaltsliste oder Browser verwendet. Er ist der primären Detailansicht zum Anzeigen von Inhalten im anderen Bereich untergeordnet.

Bild, das ein Beispiel für ein SlidingPaneLayout zeigt
Abbildung 1. Ein Beispiel für ein mit SlidingPaneLayout erstelltes Layout.

SlidingPaneLayout ermittelt anhand der Breite der beiden Bereiche, ob die Bereiche nebeneinander angezeigt werden sollen. Wenn der Listenbereich beispielsweise mit einer Mindestgröße von 200 dp gemessen wird und der Detailbereich 400 dp benötigt, zeigt SlidingPaneLayout die beiden Bereiche automatisch nebeneinander an, sofern eine Breite von mindestens 600 dp verfügbar ist.

Untergeordnete Ansichten überlappen sich, wenn ihre kombinierte Breite die verfügbare Breite in SlidingPaneLayout überschreitet. In diesem Fall werden die untergeordneten Ansichten so erweitert, dass sie die in SlidingPaneLayout verfügbare Breite ausfüllen. Der Nutzer kann die oberste Ansicht aus dem Weg schieben, indem er sie vom Bildschirmrand zurückzieht.

Wenn sich die Ansichten nicht überschneiden, unterstützt SlidingPaneLayout die Verwendung des Layoutparameters layout_weight für untergeordnete Ansichten, um zu definieren, wie der übrige Platz nach Abschluss der Messung aufgeteilt werden soll. Dieser Parameter ist nur für die Breite relevant.

Auf einem faltbaren Gerät mit Platz auf dem Bildschirm, in dem beide Ansichten nebeneinander angezeigt werden können, passt SlidingPaneLayout die Größe der beiden Bereiche automatisch so an, dass sie auf beiden Seiten einer überlappenden Faltung oder eines Scharniers positioniert werden. In diesem Fall wird die vorgegebene Breite als Mindestbreite betrachtet, die auf jeder Seite des Faltelements vorhanden sein muss. Wenn nicht genügend Platz vorhanden ist, um diese Mindestgröße beizubehalten, wechselt SlidingPaneLayout zurück, sodass die Ansichten überlappen.

Hier sehen Sie ein Beispiel für die Verwendung eines SlidingPaneLayout mit RecyclerView als linken Bereich und FragmentContainerView als primäre Detailansicht, um Inhalte aus dem linken Bereich anzuzeigen:

<!-- two_pane.xml -->
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/sliding_pane_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <!-- The first child view becomes the left pane. When the combined needed
        width, expressed using android:layout_width, doesn't fit on-screen at
        once, the right pane is permitted to overlap the left. -->

   <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/list_pane"
             android:layout_width="280dp"
             android:layout_height="match_parent"
             android:layout_gravity="start"/>

   <!-- The second child becomes the right (content) pane. In this example,
        android:layout_weight is used to expand this detail pane to consume
        leftover available space when the entire window is wide enough to fit
        the left and right pane.-->
   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/detail_container"
       android:layout_width="300dp"
       android:layout_weight="1"
       android:layout_height="match_parent"
       android:background="#ff333333"
       android:name="com.example.SelectAnItemFragment" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

In diesem Beispiel fügt das Attribut android:name für FragmentContainerView das erste Fragment dem Detailbereich hinzu. Dadurch wird sichergestellt, dass Nutzern auf Geräten mit großen Bildschirmen kein leerer rechter Bereich angezeigt wird, wenn die App zum ersten Mal gestartet wird.

Detailbereich programmatisch austauschen

Im vorherigen XML-Beispiel wird durch das Tippen auf ein Element im RecyclerView eine Änderung im Detailbereich ausgelöst. Bei Verwendung von Fragmenten ist dafür ein FragmentTransaction erforderlich, das den rechten Bereich ersetzt und open() auf der SlidingPaneLayout aufruft, um zum neu sichtbaren Fragment zu wechseln:

Kotlin

// A method on the Fragment that owns the SlidingPaneLayout,called by the
// adapter when an item is selected.
fun openDetails(itemId: Int) {
    childFragmentManager.commit {
        setReorderingAllowed(true)
        replace<ItemFragment>(R.id.detail_container,
            bundleOf("itemId" to itemId))
        // If it's already open and the detail pane is visible, crossfade
        // between the fragments.
        if (binding.slidingPaneLayout.isOpen) {
            setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
        }
    }
    binding.slidingPaneLayout.open()
}

Java

// A method on the Fragment that owns the SlidingPaneLayout, called by the
// adapter when an item is selected.
void openDetails(int itemId) {
    Bundle arguments = new Bundle();
    arguments.putInt("itemId", itemId);
    FragmentTransaction ft = getChildFragmentManager().beginTransaction()
            .setReorderingAllowed(true)
            .replace(R.id.detail_container, ItemFragment.class, arguments);
    // If it's already open and the detail pane is visible, crossfade
    // between the fragments.
    if (binding.getSlidingPaneLayout().isOpen()) {
        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
    }
    ft.commit();
    binding.getSlidingPaneLayout().open();
}

Mit diesem Code wird nicht addToBackStack() im FragmentTransaction aufgerufen. Dadurch wird vermieden, dass im Detailbereich ein Back Stack erstellt wird.

In den Beispielen auf dieser Seite wird SlidingPaneLayout direkt verwendet und Sie müssen Transaktionen für Fragmente manuell verwalten. Die Navigationskomponente bietet jedoch eine vordefinierte Implementierung eines Zweifensterlayouts über AbstractListDetailFragment. Diese API-Klasse verwendet im Hintergrund einen SlidingPaneLayout, um Ihre Listen- und Detailbereiche zu verwalten.

Auf diese Weise können Sie die Konfiguration Ihres XML-Layouts vereinfachen. Anstatt explizit ein SlidingPaneLayout und beide Bereiche zu deklarieren, benötigt dein Layout nur ein FragmentContainerView für deine AbstractListDetailFragment-Implementierung:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/two_pane_container"
        <!-- The name of your AbstractListDetailFragment implementation.-->
        android:name="com.example.testapp.TwoPaneFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        <!-- The navigation graph for your detail pane.-->
        app:navGraph="@navigation/two_pane_navigation" />
</FrameLayout>

Implementieren Sie onCreateListPaneView() und onListPaneViewCreated(), um eine benutzerdefinierte Ansicht für den Listenbereich bereitzustellen. Für den Detailbereich verwendet AbstractListDetailFragment einen NavHostFragment. Sie können also eine Navigationsgrafik definieren, die nur die im Detailbereich anzuzeigenden Ziele enthält. Anschließend können Sie NavController verwenden, um im Detailbereich zwischen den Zielen in der eigenständigen Navigationsgrafik zu wechseln:

Kotlin

fun openDetails(itemId: Int) {
    val navController = navHostFragment.navController
    navController.navigate(
        // Assume the itemId is the android:id of a destination in the graph.
        itemId,
        null,
        NavOptions.Builder()
            // Pop all destinations off the back stack.
            .setPopUpTo(navController.graph.startDestination, true)
            .apply {
                // If it's already open and the detail pane is visible,
                // crossfade between the destinations.
                if (binding.slidingPaneLayout.isOpen) {
                    setEnterAnim(R.animator.nav_default_enter_anim)
                    setExitAnim(R.animator.nav_default_exit_anim)
                }
            }
            .build()
    )
    binding.slidingPaneLayout.open()
}

Java

void openDetails(int itemId) {
    NavController navController = navHostFragment.getNavController();
    NavOptions.Builder builder = new NavOptions.Builder()
            // Pop all destinations off the back stack.
            .setPopUpTo(navController.getGraph().getStartDestination(), true);
    // If it's already open and the detail pane is visible, crossfade between
    // the destinations.
    if (binding.getSlidingPaneLayout().isOpen()) {
        builder.setEnterAnim(R.animator.nav_default_enter_anim)
                .setExitAnim(R.animator.nav_default_exit_anim);
    }
    navController.navigate(
        // Assume the itemId is the android:id of a destination in the graph.
        itemId,
        null,
        builder.build()
    );
    binding.getSlidingPaneLayout().open();
}

Die Ziele im Navigationsdiagramm des Detailbereichs dürfen nicht in einer äußeren Navigationsgrafik für die gesamte App vorhanden sein. Deeplinks im Navigationsdiagramm des Detailbereichs müssen jedoch mit dem Ziel verknüpft sein, das SlidingPaneLayout hostet. Dadurch wird sichergestellt, dass externe Deeplinks zuerst zum Ziel SlidingPaneLayout und dann zum richtigen Detailbereich gelangen.

Eine vollständige Implementierung eines Zwei-Fenster-Layouts mit der Navigationskomponente finden Sie im TwoPaneFragment-Beispiel.

Zurück-Taste des Systems integrieren

Auf kleineren Geräten, auf denen sich die Listen- und Detailbereiche überschneiden, solltest du darauf achten, dass die Systemschaltfläche „Zurück“ den Nutzer vom Detailbereich zurück zum Listenbereich weiterleitet. Dazu stellen Sie eine benutzerdefinierte Zurück-Navigation bereit und verbinden OnBackPressedCallback mit dem aktuellen Status von SlidingPaneLayout:

Kotlin

class TwoPaneOnBackPressedCallback(
    private val slidingPaneLayout: SlidingPaneLayout
) : OnBackPressedCallback(
    // Set the default 'enabled' state to true only if it is slidable, such as
    // when the panes overlap, and open, such as when the detail pane is
    // visible.
    slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen
), SlidingPaneLayout.PanelSlideListener {

    init {
        slidingPaneLayout.addPanelSlideListener(this)
    }

    override fun handleOnBackPressed() {
        // Return to the list pane when the system back button is tapped.
        slidingPaneLayout.closePane()
    }

    override fun onPanelSlide(panel: View, slideOffset: Float) { }

    override fun onPanelOpened(panel: View) {
        // Intercept the system back button when the detail pane becomes
        // visible.
        isEnabled = true
    }

    override fun onPanelClosed(panel: View) {
        // Disable intercepting the system back button when the user returns to
        // the list pane.
        isEnabled = false
    }
}

Java

class TwoPaneOnBackPressedCallback extends OnBackPressedCallback
        implements SlidingPaneLayout.PanelSlideListener {

    private final SlidingPaneLayout mSlidingPaneLayout;

    TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout) {
        // Set the default 'enabled' state to true only if it is slideable, such
        // as when the panes overlap, and open, such as when the detail pane is
        // visible.
        super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen());
        mSlidingPaneLayout = slidingPaneLayout;
        slidingPaneLayout.addPanelSlideListener(this);
    }

    @Override
    public void handleOnBackPressed() {
        // Return to the list pane when the system back button is tapped.
        mSlidingPaneLayout.closePane();
    }

    @Override
    public void onPanelSlide(@NonNull View panel, float slideOffset) { }

    @Override
    public void onPanelOpened(@NonNull View panel) {
        // Intercept the system back button when the detail pane becomes
        // visible.
        setEnabled(true);
    }

    @Override
    public void onPanelClosed(@NonNull View panel) {
        // Disable intercepting the system back button when the user returns to
        // the list pane.
        setEnabled(false);
    }
}

Sie können den Callback dem OnBackPressedDispatcher mit addCallback() hinzufügen:

Kotlin

class TwoPaneFragment : Fragment(R.layout.two_pane) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = TwoPaneBinding.bind(view)

        // Connect the SlidingPaneLayout to the system back button.
        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner,
            TwoPaneOnBackPressedCallback(binding.slidingPaneLayout))

        // Set up the RecyclerView adapter.
    }
}

Java

class TwoPaneFragment extends Fragment {

    public TwoPaneFragment() {
        super(R.layout.two_pane);
    }

    @Override
    public void onViewCreated(@NonNull View view,
             @Nullable Bundle savedInstanceState) {
        TwoPaneBinding binding = TwoPaneBinding.bind(view);

        // Connect the SlidingPaneLayout to the system back button.
        requireActivity().getOnBackPressedDispatcher().addCallback(
            getViewLifecycleOwner(),
            new TwoPaneOnBackPressedCallback(binding.getSlidingPaneLayout()));

        // Set up the RecyclerView adapter.
    }
}

Sperrmodus

Mit SlidingPaneLayout kannst du auf Smartphones immer manuell open() und close() aufrufen, um zwischen Listen- und Detailbereichen zu wechseln. Diese Methoden haben keine Auswirkungen, wenn beide Bereiche sichtbar sind und sich nicht überschneiden.

Wenn sich die Listen- und Detailbereiche überschneiden, können Nutzer standardmäßig in beide Richtungen wischen und beliebig zwischen den beiden Bereichen wechseln, auch wenn sie keine Navigation mit Gesten verwenden. Du kannst die Wischrichtung steuern, indem du den Sperrmodus von SlidingPaneLayout festlegst:

Kotlin

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Java

binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);

Weitere Informationen

Weitere Informationen zum Entwerfen von Layouts für verschiedene Formfaktoren finden Sie in der folgenden Dokumentation:

Zusätzliche Ressourcen