Ein Layout mit zwei Bereichen erstellen

Jetpack Compose
Jetpack Compose ist das empfohlene UI-Toolkit für Android. Informationen zum Arbeiten mit Layouts in Compose

Jeder Bildschirm in Ihrer App muss responsiv sein und sich an den verfügbaren Platz anpassen. Mit können Sie eine responsive UI mit ConstraintLayout erstellen, mit der ein einzelner Bereich auf viele Größen skaliert werden kann. Bei größeren Geräten kann es jedoch sinnvoll sein, das Layout in mehrere Bereiche aufzuteilen. Beispielsweise kann ein Bildschirm eine Liste von Elementen neben einer Liste mit Details zum ausgewählten Element anzeigen.

Die SlidingPaneLayout Komponente unterstützt die Anzeige von zwei Bereichen nebeneinander auf größeren Geräten und faltbaren Geräten. Auf kleineren Geräten wie Smartphones wird automatisch nur ein Bereich gleichzeitig angezeigt.

Gerätespezifische Anleitungen finden Sie in der Übersicht zur Bildschirmkompatibilität.

Einrichtung

Wenn Sie SlidingPaneLayout verwenden möchten, fügen Sie der Datei build.gradle Ihrer App die folgende Abhängigkeit hinzu:

Cool

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

Kotlin

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

XML-Layoutkonfiguration

SlidingPaneLayout bietet ein horizontales Layout mit zwei Bereichen für die oberste Ebene einer UI. Bei diesem Layout wird der erste Bereich als Inhaltsliste oder Browser verwendet, der einer primären Detailansicht untergeordnet ist, um Inhalte im anderen Bereich anzuzeigen.

Ein Bild mit einem Beispiel für SlidingPaneLayout
Abbildung 1 Beispiel für ein Layout, das mit SlidingPaneLayout erstellt wurde.

Bei SlidingPaneLayout wird anhand der Breite der beiden Bereiche bestimmt, ob die Bereiche nebeneinander angezeigt werden sollen. Wenn beispielsweise für den Listenbereich eine Mindestgröße von 200 dp und für den Detailbereich 400 dp erforderlich sind, werden die beiden Bereiche in SlidingPaneLayout automatisch nebeneinander angezeigt, sofern mindestens 600 dp Breite verfügbar sind.

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

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

Auf einem faltbaren Gerät, auf dem beide Ansichten nebeneinander angezeigt werden können, passt SlidingPaneLayout die Größe der beiden Bereiche automatisch an, sodass sie sich auf beiden Seiten einer überlappenden Faltstelle oder eines Scharniers befinden. In diesem Fall gelten die festgelegten Breiten als Mindestbreite, die auf jeder Seite der Faltstelle vorhanden sein muss. Wenn nicht genügend Platz vorhanden ist, um diese Mindestgröße beizubehalten, wechselt SlidingPaneLayout wieder zum Überlappen der Ansichten.

Hier ist ein Beispiel für die Verwendung von SlidingPaneLayout mit RecyclerView als linkem Bereich und FragmentContainerView als primärer 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 wird mit dem Attribut android:name in FragmentContainerView das erste Fragment dem Detailbereich hinzugefügt. So wird verhindert, dass Nutzern auf Geräten mit großem Bildschirm beim ersten Starten der App ein leerer rechter Bereich angezeigt wird.

Detailbereich programmatisch austauschen

Im vorherigen XML-Beispiel wird durch Tippen auf ein Element in RecyclerView eine Änderung im Detailbereich ausgelöst. Bei der Verwendung von Fragmenten ist dazu eine FragmentTransaction erforderlich, die den rechten Bereich ersetzt und open() für die 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();
}

In diesem Code wird addToBackStack() nicht für FragmentTransactionaufgerufen. So wird verhindert, dass im Detailbereich ein Back-Stack erstellt wird.

In den Beispielen auf dieser Seite wird SlidingPaneLayout direkt verwendet und Sie müssen Fragmenttransaktionen manuell verwalten. Die Navigationskomponente bietet jedoch eine vorgefertigte Implementierung eines Layouts mit zwei Bereichen über AbstractListDetailFragment, eine API-Klasse, die SlidingPaneLayout im Hintergrund verwendet, um Ihre Listen- und Detailbereiche zu verwalten.

So können Sie die XML-Layoutkonfiguration vereinfachen. Anstatt explizit SlidingPaneLayout und beide Bereiche zu deklarieren, benötigt Ihr Layout nur ein FragmentContainerView, um Ihre AbstractListDetailFragment-Implementierung zu enthalten:

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

Implement onCreateListPaneView() und onListPaneViewCreated() , um eine benutzerdefinierte Ansicht für den Listenbereich bereitzustellen. Für den Detailbereich wird in AbstractListDetailFragment ein NavHostFragment verwendet. Das bedeutet, dass Sie einen Navigations graphen definieren können, der nur die Ziele enthält, die im Detailbereich angezeigt werden sollen. Anschließend können Sie mit NavController Ihren Detailbereich zwischen den Zielen im eigenständigen Navigationsgraphen austauschen:

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 Navigationsgraphen des Detailbereichs dürfen nicht in einem äußeren, appweiten Navigationsgraphen vorhanden sein. Alle Deeplinks im Navigationsgraphen des Detailbereichs müssen jedoch an das Ziel angehängt werden, das SlidingPaneLayout hostet. So wird sichergestellt, dass externe Deeplinks zuerst zum Ziel SlidingPaneLayout und dann zum richtigen Ziel des Detailbereichs navigieren.

Eine vollständige Implementierung eines Layouts mit zwei Bereichen mit der Navigationskomponente finden Sie im Beispiel TwoPaneFragment.

Integration mit dem Button „Zurück“ des Systems

Auf kleineren Geräten, auf denen sich die Listen- und Detailbereiche überlappen, muss der Button „Zurück“ des Systems den Nutzer vom Detailbereich zurück zum Listenbereich führen. Dazu müssen Sie eine benutzerdefinierte Zurück- Navigation bereitstellen und einen OnBackPressedCallback mit dem aktuellen Status von SlidingPaneLayout verbinden:

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 zu den 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

SlidingPaneLayout können Sie immer manuell open() und close() aufrufen, um zwischen den Listen- und Detailbereichen auf Smartphones zu wechseln. Diese Methoden haben keine Auswirkungen, wenn beide Bereiche sichtbar sind und sich nicht überlappen.

Wenn sich die Listen- und Detailbereiche überlappen, können Nutzer standardmäßig in beide Richtungen wischen und frei zwischen den beiden Bereichen wechseln, auch wenn sie keine Gestennavigation verwenden. Sie können die Wischrichtung steuern, indem Sie den Sperrmodus von SlidingPaneLayout festlegen:

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