Chaque écran de votre application doit être responsif et s'adapter à l'espace disponible.
Vous pouvez créer une UI responsive avec ConstraintLayout
, qui permet à une approche à un seul volet de s'adapter à de nombreuses tailles. Toutefois, il peut être utile de diviser la mise en page en plusieurs volets pour les appareils plus grands. Par exemple, vous pouvez souhaiter qu'un écran affiche une liste d'éléments à côté d'une liste de détails de l'élément sélectionné.
Le composant SlidingPaneLayout
permet d'afficher deux volets côte à côte sur les appareils plus grands et les appareils pliables, tout en s'adaptant automatiquement pour n'afficher qu'un seul volet à la fois sur les appareils plus petits tels que les téléphones.
Pour obtenir des conseils spécifiques à l'appareil, consultez la présentation de la compatibilité des écrans.
Configuration
Pour utiliser SlidingPaneLayout
, incluez la dépendance suivante dans le fichier build.gradle
de votre application:
Groovy
dependencies { implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" }
Kotlin
dependencies { implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") }
Configuration de la mise en page XML
SlidingPaneLayout
fournit une mise en page horizontale à deux volets à utiliser au niveau supérieur d'une interface utilisateur. Cette mise en page utilise le premier volet en tant que liste de contenu ou navigateur. Elle dépend d'une vue détaillée principale pour afficher du contenu dans l'autre volet.
SlidingPaneLayout
utilise la largeur des deux volets pour déterminer s'ils doivent être affichés côte à côte. Par exemple, si le volet de liste doit avoir une taille minimale de 200 dp et que le volet de détails a besoin de 400 dp, SlidingPaneLayout
affiche automatiquement les deux volets côte à côte, à condition que sa largeur disponible soit d'au moins 600 dp.
Les vues enfants se chevauchent si leur largeur combinée dépasse la largeur disponible dans SlidingPaneLayout
. Dans ce cas, les vues enfants sont agrandies pour remplir la largeur disponible dans SlidingPaneLayout
. L'utilisateur peut faire glisser la vue la plus haute hors de la vue en la faisant glisser depuis le bord de l'écran.
Si les vues ne se chevauchent pas, SlidingPaneLayout
permet d'utiliser le paramètre de mise en page layout_weight
sur les vues enfants pour définir la façon de diviser l'espace restant une fois la mesure terminée. Ce paramètre n'est pertinent que pour la largeur.
Sur un appareil pliable disposant d'un espace sur l'écran pour afficher les deux vues côte à côte, SlidingPaneLayout
ajuste automatiquement la taille des deux volets afin qu'ils soient positionnés de chaque côté d'un pli ou d'une charnière qui se chevauchent. Dans ce cas, les largeurs définies sont considérées comme la largeur minimale qui doit exister de chaque côté de la fonctionnalité de pliage. Si l'espace est insuffisant pour maintenir cette taille minimale, SlidingPaneLayout
revient à la superposition des vues.
Voici un exemple d'utilisation d'un SlidingPaneLayout
avec un RecyclerView
comme volet de gauche et un FragmentContainerView
comme vue détaillée principale pour afficher le contenu du volet de gauche:
<!-- 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>
Dans cet exemple, l'attribut android:name
sur FragmentContainerView
ajoute le fragment initial au volet d'informations, ce qui garantit que les utilisateurs sur les appareils à grand écran ne voient pas de volet droit vide lors du premier lancement de l'application.
Remplacer le volet d'informations par programmation
Dans l'exemple XML précédent, appuyer sur un élément dans RecyclerView
déclenche un changement dans le volet d'informations. Lorsque vous utilisez des fragments, cela nécessite un FragmentTransaction
qui remplace le volet de droite, en appelant open()
sur SlidingPaneLayout
pour passer au fragment nouvellement visible:
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(); }
Ce code n'appelle pas spécifiquement addToBackStack()
sur FragmentTransaction
. Cela évite de créer une pile "Retour" dans le volet d'informations.
Implémentation du composant Navigation
Les exemples de cette page utilisent directement SlidingPaneLayout
et vous obligent à gérer manuellement les transactions de fragment. Toutefois, le composant de navigation fournit une implémentation prédéfinie d'une mise en page à deux volets via AbstractListDetailFragment
, une classe d'API qui utilise un SlidingPaneLayout
en interne pour gérer vos volets de liste et de détails.
Vous pouvez ainsi simplifier la configuration de votre mise en page XML. Au lieu de déclarer explicitement un SlidingPaneLayout
et vos deux volets, votre mise en page n'a besoin que d'un FragmentContainerView
pour contenir votre implémentation AbstractListDetailFragment
:
<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>
Implémentez onCreateListPaneView()
et onListPaneViewCreated()
pour fournir une vue personnalisée pour votre volet de liste. Pour le volet d'informations, AbstractListDetailFragment
utilise un NavHostFragment
.
Cela signifie que vous pouvez définir un graphique de navigation qui ne contient que les destinations à afficher dans le volet d'informations. Vous pouvez ensuite utiliser NavController
pour permuter le volet d'informations entre les destinations du graphique de navigation autonome:
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(); }
Les destinations du graphique de navigation du volet d'informations ne doivent pas figurer dans un graphique de navigation externe, à l'échelle de l'application. Toutefois, tous les liens profonds du graphique de navigation du volet d'informations doivent être associés à la destination qui héberge SlidingPaneLayout
. Cela permet de s'assurer que les liens profonds externes accèdent d'abord à la destination SlidingPaneLayout
, puis à la destination du volet d'informations appropriée.
Consultez l'exemple TwoPaneFragment pour obtenir une implémentation complète d'une mise en page à deux volets à l'aide du composant Navigation.
Intégrer au bouton "Retour" du système
Sur les appareils de plus petite taille sur lesquels les volets de liste et de détails se chevauchent, assurez-vous que le bouton "Retour" du système fait revenir l'utilisateur du volet de détails au volet de liste. Pour ce faire, fournissez une navigation vers l'arrière personnalisée et connectez un OnBackPressedCallback
à l'état actuel de 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); } }
Vous pouvez ajouter le rappel à OnBackPressedDispatcher
à l'aide de addCallback()
:
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. } }
Mode verrouillé
SlidingPaneLayout
vous permet toujours d'appeler manuellement open()
et close()
pour basculer entre les volets de liste et de détail sur les téléphones. Ces méthodes n'ont aucun effet si les deux volets sont visibles et ne se chevauchent pas.
Lorsque les volets de liste et de détails se chevauchent, les utilisateurs peuvent, par défaut, balayer l'écran dans les deux sens et basculer librement entre les deux volets, même s'ils n'utilisent pas la navigation par gestes. Vous pouvez contrôler la direction du balayage en définissant le mode verrouillé de SlidingPaneLayout
:
Kotlin
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
Java
binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);
En savoir plus
Pour en savoir plus sur la conception de mises en page pour différents facteurs de forme, consultez la documentation suivante:
- Présentation de la compatibilité des écrans
- Concevoir une solution adaptée à différents facteurs de forme