Creare un layout a due riquadri

Prova la modalità Scrivi
Jetpack Compose è il toolkit dell'interfaccia utente consigliato per Android. Scopri come utilizzare i layout in Compose.

Ogni schermata dell'app deve essere reattiva e adattarsi allo spazio disponibile. Puoi creare un'interfaccia utente adattabile con ConstraintLayout che consenta un approccio basato su un singolo riquadro scalabile su molte dimensioni, ma i dispositivi più grandi potrebbero trarre vantaggio dalla suddivisione del layout in più riquadri. Ad esempio, potresti voler visualizzare una schermata con un elenco di elementi accanto a un elenco di dettagli dell'elemento selezionato.

Il componente SlidingPaneLayout supporta la visualizzazione di due riquadri affiancati su dispositivi più grandi e pieghevoli e si adatta automaticamente in modo da mostrare un solo riquadro alla volta su dispositivi più piccoli come gli smartphone.

Per indicazioni specifiche per il dispositivo, consulta la panoramica sulla compatibilità dello schermo.

Configura

Per utilizzare SlidingPaneLayout, includi la seguente dipendenza nel file build.gradle dell'app:

Trendy

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

Kotlin

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

Configurazione del layout XML

SlidingPaneLayout offre un layout orizzontale a due riquadri da utilizzare nel livello superiore di una UI. Questo layout utilizza il primo riquadro come elenco di contenuti o browser, subordinato a una visualizzazione dei dettagli principale per mostrare i contenuti nell'altro riquadro.

Un'immagine che mostra un esempio di SlidingPaneLayout
Figura 1. Esempio di layout creato con SlidingPaneLayout.

SlidingPaneLayout utilizza la larghezza dei due riquadri per determinare se mostrare i riquadri uno accanto all'altro. Ad esempio, se si misura che la dimensione minima del riquadro dell'elenco è di 200 dp e il riquadro dei dettagli ne richiede 400, SlidingPaneLayout mostra automaticamente i due riquadri uno accanto all'altro purché siano disponibili almeno 600 dp di larghezza.

Le visualizzazioni secondarie si sovrappongono se la larghezza combinata supera la larghezza disponibile in SlidingPaneLayout. In questo caso, le visualizzazioni secondarie si espandono per riempire la larghezza disponibile in SlidingPaneLayout. L'utente può far scorrere la vista più in alto trascinandola di nuovo dal bordo dello schermo.

Se le visualizzazioni non si sovrappongono, SlidingPaneLayout supporta l'utilizzo del parametro di layout layout_weight nelle viste secondarie per definire come suddividere lo spazio rimanente al termine della misurazione. Questo parametro è pertinente solo per la larghezza.

Su un dispositivo pieghevole che ha spazio sullo schermo per mostrare entrambe le visualizzazioni affiancate, SlidingPaneLayout regola automaticamente le dimensioni dei due riquadri in modo che siano posizionati su entrambi i lati di una piega o di una cerniera sovrapposta. In questo caso, le larghezze impostate sono considerate la larghezza minima che deve esistere su ciascun lato della funzionalità di piegatura. Se lo spazio non è sufficiente per mantenere la dimensione minima, SlidingPaneLayout torna alla visualizzazione sovrapposta.

Ecco un esempio di utilizzo di un elemento SlidingPaneLayout con RecyclerView come riquadro sinistro e FragmentContainerView come visualizzazione dei dettagli principale per visualizzare i contenuti nel riquadro a sinistra:

<!-- 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 questo esempio, l'attributo android:name su FragmentContainerView aggiunge il frammento iniziale al riquadro dei dettagli, assicurando che gli utenti su dispositivi con schermi di grandi dimensioni non vedano un riquadro a destra vuoto al primo avvio dell'app.

Scambiare in modo programmatico il riquadro dei dettagli

Nell'esempio XML precedente, il tocco di un elemento in RecyclerView attiva una modifica nel riquadro dei dettagli. Quando utilizzi i frammenti, è necessario un elemento FragmentTransaction che sostituisca il riquadro di destra, chiamando open() sul SlidingPaneLayout per passare al frammento appena visibile:

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();
}

Questo codice nello specifico non chiama addToBackStack() sul FragmentTransaction. Ciò evita di creare uno stack posteriore nel riquadro dei dettagli.

Gli esempi in questa pagina utilizzano direttamente SlidingPaneLayout e richiedono la gestione manuale delle transazioni con frammenti. Tuttavia, il componente Navigazione fornisce un'implementazione predefinita di un layout a due riquadri tramite AbstractListDetailFragment, una classe API che utilizza SlidingPaneLayout in background per gestire l'elenco e i riquadri dei dettagli.

Ciò ti consente di semplificare la configurazione del layout XML. Anziché dichiarare esplicitamente SlidingPaneLayout ed entrambi i riquadri, il tuo layout necessita solo di un FragmentContainerView per contenere l'implementazione 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>

Implementa onCreateListPaneView() e onListPaneViewCreated() per fornire una visualizzazione personalizzata per il riquadro elenco. Per il riquadro dei dettagli, AbstractListDetailFragment utilizza un NavHostFragment. Ciò significa che puoi definire un grafico di navigazione contenente solo le destinazioni da mostrare nel riquadro dei dettagli. Quindi, puoi utilizzare NavController per scambiare il riquadro dei dettagli tra le destinazioni nel grafico di navigazione autonomo:

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();
}

Le destinazioni nel grafico di navigazione del riquadro dei dettagli non devono essere presenti in nessun grafico di navigazione esterno a livello di app. Tuttavia, tutti i link diretti all'interno del grafico di navigazione del riquadro dei dettagli devono essere associati alla destinazione che ospita SlidingPaneLayout. In questo modo puoi garantire che i link diretti esterni accedano prima alla destinazione SlidingPaneLayout e poi a quella corretta del riquadro dei dettagli.

Vedi l'esempio di TwoPaneFragment per un'implementazione completa di un layout a due riquadri utilizzando il componente Navigazione.

Integrazione con il pulsante Indietro del sistema

Sui dispositivi più piccoli in cui l'elenco e i riquadri dei dettagli si sovrappongono, assicurati che il pulsante Indietro del sistema riporti l'utente dal riquadro dei dettagli al riquadro dell'elenco. Per farlo, fornisci una navigazione a ritroso personalizzata e collega un OnBackPressedCallback allo stato attuale di 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);
    }
}

Puoi aggiungere il callback a OnBackPressedDispatcher utilizzando 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.
    }
}

Modalità di blocco

SlidingPaneLayout ti consente sempre di chiamare manualmente open() e close() per passare dall'elenco ai riquadri dei dettagli sui telefoni. Questi metodi non hanno alcun effetto se entrambi i riquadri sono visibili e non si sovrappongono.

Quando l'elenco e i riquadri dei dettagli si sovrappongono, gli utenti possono scorrere in entrambe le direzioni per impostazione predefinita, passando liberamente da un riquadro all'altro anche quando non utilizzano la navigazione tramite gesti. Puoi controllare la direzione di scorrimento impostando la modalità di blocco di SlidingPaneLayout:

Kotlin

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Java

binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);

Scopri di più

Per scoprire di più sulla progettazione di layout per diversi fattori di forma, consulta la seguente documentazione:

Risorse aggiuntive