Créer une mise en page à deux volets

Essayer Compose
Jetpack Compose est le kit d'outils d'interface utilisateur recommandé pour Android. Découvrez comment utiliser les mises en page dans Compose.

Chaque écran de votre application doit être responsif et s'adapter à l'espace disponible. Vous pouvez créer une interface utilisateur responsive avec ConstraintLayout qui permet à une approche à volet unique de s'adapter à de nombreuses tailles, mais il peut être utile de diviser la mise en page en plusieurs volets pour les appareils plus grands. Par exemple, vous pouvez afficher une liste d'éléments à côté d'une liste d'informations sur 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 à votre appareil, consultez la présentation de la compatibilité des écrans.

Configurer

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, subordonné à une vue détaillée principale pour afficher le contenu de l'autre volet.

Image montrant un exemple de SlidingPaneLayout
Figure 1 : Exemple de mise en page créée avec SlidingPaneLayout.

SlidingPaneLayout utilise la largeur des deux volets pour déterminer s'ils doivent être affichés côte à côte. Par exemple, si la taille minimale du volet de liste est de 200 dp et que la taille du volet des détails est de 400 dp, SlidingPaneLayout affiche automatiquement les deux volets côte à côte, à condition qu'ils disposent d'une largeur 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 se développent pour remplir la largeur disponible dans SlidingPaneLayout. L'utilisateur peut faire glisser la vue supérieure hors du champ de vision en la faisant glisser vers l'arrière à partir du bord de l'écran.

Si les vues ne se chevauchent pas, SlidingPaneLayout prend en charge l'utilisation du paramètre de mise en page layout_weight sur les vues enfants pour définir comment diviser l'espace restant une fois la mesure terminée. Ce paramètre ne concerne que la largeur.

Sur un appareil pliable qui dispose d'espace à 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 devant exister de chaque côté de la fonctionnalité de pliage. Si l'espace est insuffisant pour conserver cette taille minimale, SlidingPaneLayout bascule à nouveau sur les vues.

Voici un exemple d'utilisation d'un élément SlidingPaneLayout ayant un RecyclerView comme volet 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 des détails, ce qui garantit que les utilisateurs d'appareils à grand écran ne voient pas un volet de droite vide au premier lancement de l'application.

Remplacer le volet de détails par programmation

Dans l'exemple XML précédent, le fait d'appuyer sur un élément de RecyclerView déclenche une modification dans le volet de détails. Lorsque vous utilisez des fragments, cela nécessite un élément FragmentTransaction qui remplace le volet de droite, en appelant open() sur SlidingPaneLayout pour passer au nouveau fragment 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 de détails.

Les exemples de cette page utilisent directement SlidingPaneLayout et vous obligent à gérer les transactions de fragment manuellement. Toutefois, le composant Navigation fournit une implémentation prédéfinie d'une mise en page à deux volets via AbstractListDetailFragment, une classe d'API qui utilise SlidingPaneLayout en arrière-plan pour gérer vos volets de liste et de détails.

Cela vous permet de 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() afin de fournir une vue personnalisée pour votre volet de liste. Pour le volet des détails, AbstractListDetailFragment utilise un NavHostFragment. Cela signifie que vous pouvez définir un graphique de navigation ne contenant que les destinations à afficher dans le volet de détails. Vous pouvez ensuite utiliser NavController pour permuter votre volet de détails 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 de détails ne doivent pas être présentes dans un graphique de navigation externe à l'échelle de l'application. Toutefois, tous les liens profonds du graphique de navigation du volet de détails doivent être associés à la destination qui héberge le SlidingPaneLayout. Cela permet de s'assurer que les liens profonds externes redirigent d'abord vers la destination SlidingPaneLayout, puis vers la destination correcte du volet de détails.

Consultez l'exemple TwoPaneFragment pour découvrir l'implémentation complète d'une mise en page à deux volets à l'aide du composant Navigation.

Intégration avec le bouton "Retour" du système

Sur les appareils de plus petite taille sur lesquels les volets de liste et de détail se chevauchent, assurez-vous que le bouton "Retour" du système redirige l'utilisateur du volet des détails vers le 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 passer d'un volet de liste à un volet détaillé sur un téléphone. 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étail se chevauchent, les utilisateurs peuvent balayer l'écran dans les deux sens par défaut et basculer librement entre les deux volets, même sans utiliser 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:

Ressources supplémentaires