Jeder Bildschirm in Ihrer App muss responsiv sein und sich an den verfügbaren Platz anpassen.
Sie können eine responsive UI mit ConstraintLayout
erstellen, mit der sich in einem einzigen Bereich eine Skalierung auf viele Größen vornehmen lässt. Größere Geräte können jedoch von einer Aufteilung des Layouts in mehrere Bereiche profitieren. Angenommen, auf einem Bildschirm soll neben einer Liste mit Details des ausgewählten Elements eine Liste von Elementen angezeigt werden.
Die Komponente SlidingPaneLayout
unterstützt auf größeren Geräten und faltbaren Geräten zwei Bereiche nebeneinander, in denen auf kleineren Geräten wie Smartphones jeweils nur ein Bereich angezeigt wird.
Gerätespezifische Anleitungen finden Sie in der Übersicht zur Bildschirmkompatibilität.
Einrichten
Wenn du SlidingPaneLayout
verwenden möchtest, füge die folgende Abhängigkeit in die Datei build.gradle
deiner App 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 für die Verwendung auf der obersten Ebene einer UI. Bei diesem Layout wird der erste Bereich als Inhaltsliste oder Browser verwendet, der einer primären Detailansicht zur Anzeige von Inhalten im anderen Bereich untergeordnet ist.
SlidingPaneLayout
bestimmt anhand der Breite der beiden Bereiche, ob die Bereiche nebeneinander angezeigt werden sollen. Wenn der Listenbereich beispielsweise eine Mindestgröße von 200 dp hat und der Detailbereich 400 dp benötigt, werden in SlidingPaneLayout
automatisch die beiden Bereiche nebeneinander angezeigt, sofern eine Breite von mindestens 600 dp verfügbar ist.
Untergeordnete Ansichten überschneiden sich, wenn ihre kombinierte Breite die verfügbare Breite in SlidingPaneLayout
überschreitet. In diesem Fall werden die untergeordneten Ansichten maximiert und nehmen die verfügbare Breite in SlidingPaneLayout
ein. Die oberste Ansicht lässt sich durch Ziehen
vom Rand des Bildschirms zurückbewegen.
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 werden soll. Dieser Parameter ist nur für die Breite relevant.
Auf einem faltbaren Gerät mit Platz auf dem Bildschirm, um beide Ansichten nebeneinander anzuzeigen, passt SlidingPaneLayout
die Größe der beiden Fenster automatisch so an, dass sie auf beiden Seiten einer überlappenden Faltung oder eines Scharniers positioniert sind. In diesem Fall gelten die festgelegten Breiten als die 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 zur Überlappung der Ansichten.
Hier ist ein Beispiel für die Verwendung eines SlidingPaneLayout
-Objekts, das einen RecyclerView
als linken Bereich und eine FragmentContainerView
als primäre Detailansicht hat, 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 durch das Attribut android:name
für FragmentContainerView
das anfängliche Fragment zum Detailbereich hinzugefügt. Dadurch wird sichergestellt, dass Nutzer auf Geräten mit großen Bildschirmen beim ersten Start der App keinen leeren rechten Bereich sehen.
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 ein FragmentTransaction
erforderlich, der den rechten Bereich ersetzt und open()
auf dem SlidingPaneLayout
-Element aufgerufen wird, 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(); }
Durch diesen Code wird insbesondere addToBackStack()
für FragmentTransaction
nicht aufgerufen. Dadurch wird im Detailbereich kein Back-Stack erstellt.
Implementierung der Navigationskomponente
In den Beispielen auf dieser Seite wird SlidingPaneLayout
direkt verwendet. Sie müssen Fragmenttransaktionen daher manuell verwalten. Die Navigationskomponente bietet jedoch eine vorgefertigte Implementierung eines Zweifensterlayouts über AbstractListDetailFragment
. Dabei handelt es sich um eine API-Klasse, die im Hintergrund ein SlidingPaneLayout
verwendet, um Ihre Liste und Detailbereiche zu verwalten.
So können Sie die Konfiguration Ihres XML-Layouts vereinfachen. Anstatt SlidingPaneLayout
und beide Bereiche explizit zu deklarieren, benötigt das Layout nur eine FragmentContainerView
für die 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 Ziele enthält, die im Detailbereich angezeigt werden sollen. Anschließend können Sie mit NavController
im Detailbereich 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 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 an das Ziel angehängt sein, das das SlidingPaneLayout
hostet. So sorgen Sie dafür, dass externe Deeplinks zuerst zum Ziel SlidingPaneLayout
und dann zum richtigen Ziel im Detailbereich navigieren.
Eine vollständige Implementierung eines Zwei-Fenster-Layouts mithilfe der Navigationskomponente finden Sie im TwoPaneFragment-Beispiel.
Schaltfläche „Zurück“ des Systems einbinden
Auf kleineren Geräten, auf denen sich die Listen- und Detailbereiche überschneiden, muss die Systemschaltfläche „Zurück“ den Nutzer vom Detailbereich zurück zum Listenbereich führen. Dazu stellen Sie eine benutzerdefinierte Zurücknavigation bereit und verbinden eine 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); } }
Du kannst den Callback mit addCallback()
dem 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 open()
und close()
immer manuell aufrufen, um zwischen der Liste und den Detailbereichen auf Smartphones 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 dabei frei zwischen den beiden Bereichen wechseln, auch wenn sie keine Bedienung über Gesten 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: