Tworzenie układu z 2 panelami

Wypróbuj sposób tworzenia wiadomości
Jetpack Compose to zalecany zestaw narzędzi UI na Androida. Dowiedz się, jak korzystać z układów w funkcji Utwórz

Każdy ekran w aplikacji musi reagować na ruch i dostosowywać się do dostępnego miejsca. Możesz utworzyć elastyczny interfejs użytkownika za pomocą narzędzia ConstraintLayout, które umożliwia skalowanie do wielu rozmiarów w jednym panelu, ale podzielenie układu na wiele paneli może być korzystne dla większych urządzeń. Możesz na przykład ustawić na ekranie listę elementów obok listy szczegółów wybranego elementu.

Na większych i składanych komponentach SlidingPaneLayout może wyświetlać obok siebie 2 panele i automatycznie dostosowywać się do wyświetlania tylko jednego panelu naraz na mniejszych urządzeniach, takich jak telefony.

Wskazówki dotyczące poszczególnych urządzeń znajdziesz w artykule Omówienie zgodności ekranów.

Konfiguruj

Aby używać funkcji SlidingPaneLayout, w pliku build.gradle aplikacji dodaj tę zależność:

Odlotowy

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

Kotlin

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

Konfiguracja układu XML

SlidingPaneLayout udostępnia poziomy z 2 panelami do użytku na najwyższym poziomie interfejsu użytkownika. Ten układ używa pierwszego panelu jako listy treści lub przeglądarki i podlega głównemu widokowi szczegółów do wyświetlania treści w drugim panelu.

Obraz przedstawiający przykładowy układ SlidingPaneLayout
Rysunek 1. Przykład układu utworzonego za pomocą SlidingPaneLayout.

SlidingPaneLayout wykorzystuje szerokość 2 paneli do określenia, czy wyświetlić je obok siebie. Jeśli na przykład mierzy się, że minimalny rozmiar panelu listy wynosi 200 dp, a okien szczegółów – 400 dp, SlidingPaneLayout automatycznie wyświetla te 2 panele obok siebie, o ile mają one szerokość co najmniej 600 dp.

Widoki podrzędne zachodzą na siebie, jeśli ich łączna szerokość przekracza szerokość dostępną w narzędziu SlidingPaneLayout. W takim przypadku widoki podrzędne rozwijają się, aby wypełnić dostępną szerokość w elemencie SlidingPaneLayout. Użytkownik może przesunąć widok górny z widoku, przeciągając go z powrotem od krawędzi ekranu.

Jeśli widoki się nie nakładają, SlidingPaneLayout obsługuje w widokach podrzędnych parametr układu layout_weight, by określić sposób dzielenia pozostałego miejsca po zakończeniu pomiaru. Ten parametr ma znaczenie tylko w przypadku szerokości.

Na urządzeniu składanym, na którym na ekranie jest miejsce, aby można było wyświetlać oba widoki obok siebie, SlidingPaneLayout automatycznie dostosowuje rozmiar 2 paneli tak, aby znajdowały się po obu stronach nakładającego się na siebie lub zawiasu. W takim przypadku ustawione szerokości są uznawane za minimalną szerokość, która musi istnieć po każdej stronie funkcji zwijania. Jeśli nie ma dość miejsca, by utrzymać minimalny rozmiar, SlidingPaneLayout przełącza się z powrotem na nakładające się widoki.

Oto przykład użycia obiektu SlidingPaneLayout, w którym panel po lewej stronie to RecyclerView, a głównym widokiem szczegółów jest FragmentContainerView:

<!-- 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>

W tym przykładzie atrybut android:name w domenie FragmentContainerView dodaje początkowy fragment do panelu szczegółów, dzięki czemu użytkownicy urządzeń z dużym ekranem nie widzą pustego panelu po prawej stronie przy pierwszym uruchomieniu aplikacji.

Automatycznie zamień okienko szczegółów

W poprzednim przykładzie kodu XML kliknięcie elementu w tabeli RecyclerView powoduje zmianę w panelu szczegółów. Gdy używasz fragmentów, element FragmentTransaction zastępuje prawy panel, wywołując element open() w elemencie SlidingPaneLayout w celu zamiany na nowo widoczny fragment:

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

Ten kod nie wywołuje addToBackStack() w FragmentTransaction. Pozwala to uniknąć tworzenia stosu wstecznego w panelu szczegółów.

W przykładach na tej stronie bezpośrednio używane są SlidingPaneLayout i wymagają ręcznego zarządzania transakcjami związanymi z fragmentami. Komponent nawigacyjny udostępnia jednak gotową implementację układu z 2 panelami w AbstractListDetailFragment, czyli klasie interfejsu API, która używa wewnętrznego interfejsu SlidingPaneLayout do zarządzania panelami list i szczegółów.

Pozwala to uprościć konfigurację układu XML. Zamiast jednoznacznie deklarować element SlidingPaneLayout i oba panele, układ wymaga tylko właściwości FragmentContainerView do przechowywania implementacji 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>

Zaimplementuj parametry onCreateListPaneView() i onListPaneViewCreated(), aby udostępnić widok niestandardowy panelu listy. W panelu szczegółów AbstractListDetailFragment używa NavHostFragment. Oznacza to, że możesz zdefiniować wykres nawigacyjny zawierający tylko miejsca docelowe wyświetlane w panelu szczegółów. Następnie za pomocą narzędzia NavController możesz przełączać się w okienku szczegółów między miejscami docelowymi na autonomicznym wykresie nawigacji:

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

Miejsca docelowe z wykresu nawigacji w panelu szczegółów nie mogą być widoczne na żadnym zewnętrznym wykresie nawigacji obejmującym całą aplikację. Jednak wszystkie precyzyjne linki na wykresie nawigacyjnym panelu szczegółów muszą być dołączone do miejsca docelowego, w którym znajduje się obiekt SlidingPaneLayout. Dzięki temu zewnętrzne precyzyjne linki najpierw przechodzą do miejsca docelowego SlidingPaneLayout, a potem do właściwego miejsca docelowego w panelu szczegółów.

Pełną implementację układu z dwoma panelami z użyciem komponentu Nawigacja znajdziesz w przykładzie TwoPaneFragment.

Integracja z systemowym przyciskiem Wstecz

Na mniejszych urządzeniach, na których panele listy i szczegółów nakładają się na siebie, dopilnuj, aby przycisk Wstecz przesuwał użytkownika z panelu szczegółów do panelu listy. Aby to zrobić, udostępnij niestandardową nawigację wsteczną i połącz element OnBackPressedCallback z aktualnym stanem obiektu 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);
    }
}

Wywołanie zwrotne możesz dodać do OnBackPressedDispatcher za pomocą 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.
    }
}

Tryb blokady

SlidingPaneLayout pozwala zawsze ręcznie wywoływać na telefonach interfejsy open() i close(), aby przechodzić między okienkami listy i szczegółów. Metody te nie działają, jeśli oba panele są widoczne i nie nakładają się na siebie.

Gdy panele listy i szczegółów nakładają się na siebie, użytkownicy mogą domyślnie przesuwać palcem w obu kierunkach, swobodnie przełączać się między nimi nawet wtedy, gdy nie korzysta z nawigacji przy użyciu gestów. Kierunkiem przesuwania możesz sterować, ustawiając tryb blokady na urządzeniu SlidingPaneLayout:

Kotlin

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Java

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

Więcej informacji

Więcej informacji o projektowaniu układów na potrzeby różnych formatów znajdziesz w tej dokumentacji:

Dodatkowe materiały