Alle Bildschirme in Ihrer App müssen responsiv sein und sich an den verfügbaren Platz anpassen.
Sie können mit ConstraintLayout
eine responsive Benutzeroberfläche erstellen, mit der sich ein einspaltiges Layout auf viele Größen skalieren lässt. Bei größeren Geräten kann es jedoch von Vorteil sein, das Layout in mehrere Bereiche zu unterteilen. Sie können beispielsweise auf einem Bildschirm eine Liste mit Elementen neben einer Liste mit Details zum ausgewählten Element anzeigen lassen.
Die Komponente SlidingPaneLayout
unterstützt die Anzeige von zwei Ansichten nebeneinander auf größeren Geräten und faltbaren Geräten. Auf kleineren Geräten wie Smartphones wird automatisch nur eine Ansicht angezeigt.
Gerätespezifische Informationen finden Sie in der Übersicht zur Bildschirmkompatibilität.
Einrichten
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 Ansichten, das in der obersten Ebene einer Benutzeroberfläche verwendet werden kann. 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.
SlidingPaneLayout
bestimmt anhand der Breite der beiden Bereiche, ob sie nebeneinander angezeigt werden. Wenn der Listenbereich beispielsweise eine Mindestgröße von 200 dp hat und der Detailbereich 400 dp benötigt, werden in SlidingPaneLayout
die beiden Bereiche automatisch nebeneinander angezeigt, sofern eine Breite von mindestens 600 dp verfügbar ist.
Untergeordnete Ansichten überlappen sich, wenn ihre kombinierte Breite die verfügbare Breite in der SlidingPaneLayout
überschreitet. In diesem Fall werden die untergeordneten Ansichten maximiert, um die verfügbare Breite in der SlidingPaneLayout
auszufüllen. Der Nutzer kann die oberste Ansicht aus dem Weg schieben, indem er sie vom Bildschirmrand wegzieht.
Wenn sich die Ansichten nicht überschneiden, unterstützt SlidingPaneLayout
die Verwendung des Layoutparameters layout_weight
für untergeordnete Ansichten, um zu definieren, wie der verbleibende Platz nach Abschluss der Messung aufgeteilt wird. 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 Ansichten automatisch so an, dass sie sich auf beiden Seiten einer überlappenden Falz oder eines Scharniers befinden. In diesem Fall gelten die festgelegten Breiten als Mindestbreite, die auf jeder Seite der Faltfunktion vorhanden sein muss. Wenn nicht genügend Platz vorhanden ist, um diese Mindestgröße beizubehalten, wechselt SlidingPaneLayout
wieder zum Überlappen der Ansichten.
Hier ein Beispiel für eine SlidingPaneLayout
mit einem RecyclerView
im linken Bereich und einer 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 wird mit dem android:name
-Attribut auf FragmentContainerView
das erste Fragment in den Detailbereich eingefügt, damit Nutzer auf Geräten mit großem Bildschirm beim ersten Start der App keinen leeren rechten Bereich sehen.
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 ein FragmentTransaction
erforderlich, der den rechten Bereich ersetzt. Über das SlidingPaneLayout
wird open()
aufgerufen, 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 auf dem FragmentTransaction
aufgerufen. So wird verhindert, dass im Detailbereich ein Rückstapel erstellt wird.
Implementierung der Navigationskomponente
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 zweispaltigen Layouts über AbstractListDetailFragment
, eine API-Klasse, die unter der Haube ein SlidingPaneLayout
verwendet, um Listen- und Detailbereiche zu verwalten.
So lässt sich die XML-Layoutkonfiguration vereinfachen. Anstatt SlidingPaneLayout
und beide Bereiche explizit zu deklarieren, benötigt Ihr Layout nur eine FragmentContainerView
, um die 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>
Implementieren Sie onCreateListPaneView()
und onListPaneViewCreated()
, um eine benutzerdefinierte Ansicht für den Listenbereich bereitzustellen. Für den Detailbereich verwendet AbstractListDetailFragment
ein NavHostFragment
.
Sie können also einen Navigationsgraphen definieren, der nur die Ziele enthält, die im Detailbereich angezeigt werden sollen. Anschließend können Sie mit der Taste NavController
zwischen den Zielen im eigenständigen Navigationsdiagramm 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 Navigationsgraphen des Detailbereichs dürfen nicht in einem externen, app-weiten Navigationsgraphen vorhanden sein. Alle Deeplinks im Navigationsgraphen des Detailbereichs müssen jedoch mit dem Ziel verknüpft sein, auf dem die SlidingPaneLayout
gehostet wird. So wird sichergestellt, dass externe Deeplinks zuerst zum SlidingPaneLayout
-Ziel und dann zum richtigen Detailbereich-Ziel weitergeleitet werden.
Im Beispiel TwoPaneFragment finden Sie eine vollständige Implementierung eines zweispaltigen Layouts mit der Navigationskomponente.
Einbindung in die Zurück-Schaltfläche des Systems
Achten Sie bei kleineren Geräten, auf denen sich die Listen- und Detailbereiche überschneiden, darauf, dass der Nutzer über die Schaltfläche „Zurück“ des Systems vom Detailbereich zum Listenbereich zurückkehren kann. Dazu muss eine benutzerdefinierte Rücknavigation bereitgestellt und eine OnBackPressedCallback
mit dem aktuellen Status der SlidingPaneLayout
verknüpft werden:
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 Rückruf mit addCallback()
der OnBackPressedDispatcher
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
können Sie auf Smartphones jederzeit manuell open()
und close()
aufrufen, um zwischen Listen- und Detailbereich 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 auch ohne Gestennavigation frei zwischen den beiden Bereichen wechseln. Sie können die Wischrichtung steuern, indem Sie den Sperrmodus der 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: