Tworzenie układu z 2 panelami

Wypróbuj tworzenie wiadomości
Jetpack Compose to zalecany zestaw narzędzi interfejsu na Androida. Dowiedz się, jak pracować z układami w Compose.

Każdy ekran w aplikacji musi być elastyczny i dostosowywać się do dostępnej przestrzeni. Możesz utworzyć elastyczny interfejs użytkownika za pomocą ConstraintLayout, który pozwala na skalowanie na wiele rozmiarów w jednym panelu, ale na większych urządzeniach lepiej jest podzielić układ na kilka paneli. Możesz na przykład wyświetlić na ekranie listę elementów obok listy szczegółów wybranego elementu.

Komponent SlidingPaneLayout umożliwia wyświetlanie dwóch paneli obok siebie na większych urządzeniach i urządzeniach składanych, a także automatyczne dostosowywanie 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 z ekranami.

Konfiguracja

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

Odlotowe

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

Kotlin

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

Konfiguracja układu XML

SlidingPaneLayout zapewnia poziomy układ z 2 panelami do użytku na najwyższym poziomie interfejsu. W tym układzie pierwsza cześć służy jako lista treści lub przeglądarka, która jest podrzędna głównemu widokowi szczegółów wyświetlającemu treści w drugiej części.

Ilustracja pokazująca przykład układu SlidingPane
Rysunek 1. Przykład układu utworzonego za pomocą SlidingPaneLayout.

SlidingPaneLayout określa szerokość obu paneli, aby określić, czy mają być wyświetlane obok siebie. Jeśli na przykład minimalny rozmiar panelu listy wynosi 200 dp, a panel szczegółów wymaga 400 dp, komponentSlidingPaneLayout automatycznie wyświetla oba panele obok siebie, o ile ma do dyspozycji co najmniej 600 dp szerokości.

Widoki podrzędnych nakładają się, jeśli ich łączna szerokość przekracza dostępną szerokość w SlidingPaneLayout. W tym przypadku widoki dziecięce rozszerzają się, aby wypełnić dostępną szerokość w SlidingPaneLayout. Użytkownik może przesunąć widok z najwyższego poziomu, przeciągając go z poziomu krawędzi ekranu.

Jeśli widoki się nie pokrywają, SlidingPaneLayout obsługuje użycie parametru układu layout_weight w widokach podrzędnych, aby określić, jak podzielić pozostałą przestrzeń po zakończeniu pomiaru. Ten parametr ma znaczenie tylko w przypadku szerokości.

Na urządzeniu składanym, które ma na ekranie miejsce na wyświetlanie obu widoków obok siebie, SlidingPaneLayout automatycznie dostosowuje rozmiar obu paneli, aby były one umieszczone po obu stronach nachodzących na siebie zawiasów. W tym przypadku ustawione szerokości są uważane za minimalną szerokość, jaka musi występować po każdej stronie funkcji składania. Jeśli nie ma wystarczającej ilości miejsca, aby zachować ten minimalny rozmiar, SlidingPaneLayout przełączy się z powrotem na nakładanie widoków.

Oto przykład użycia elementu SlidingPaneLayout, który ma RecyclerView w lewym panelu i FragmentContainerView jako główny widok szczegółów, aby wyświetlać treści z lewego panelu:

<!-- 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 elementach FragmentContainerView dodaje początkowy fragment do panelu szczegółów, dzięki czemu użytkownicy na urządzeniach z dużym ekranem nie widzą pustego prawego panelu po pierwszym uruchomieniu aplikacji.

Zastępowanie panelu szczegółów za pomocą kodu

W powyższym przykładzie kodu XML kliknięcie elementu w elementie RecyclerViewpowoduje zmianę w panelu szczegółów. W przypadku użycia fragmentów wymaga to utworzenia elementu FragmentTransaction, który zastąpi panel po prawej stronie. Aby wywołać element SlidingPaneLayout, należy kliknąć przycisk SlidingPaneLayout, aby przejść do nowo wyświetlanego fragmentu:open()

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 funkcji addToBackStack() w obiekcie FragmentTransaction. Dzięki temu nie trzeba tworzyć stosu wstecznego w panelu szczegółów.

Przykłady na tej stronie używają bezpośrednio funkcji SlidingPaneLayout i wymagają ręcznego zarządzania transakcjami fragmentów. Jednak komponent nawigacji udostępnia gotową implementację układu z 2 panelami za pomocą klasy interfejsu API AbstractListDetailFragment, która pod spodem korzysta z interfejsu SlidingPaneLayout do zarządzania panelami listy i szczegółów.

Dzięki temu możesz uprościć konfigurację układu XML. Zamiast deklarowania SlidingPaneLayout i obu paneli, układ potrzebuje tylko elementu FragmentContainerView, który będzie zawierać implementację 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>

Wprowadź widok niestandardowy w panelu listy, korzystając z elementów onCreateListPaneView()onListPaneViewCreated(). W panelu szczegółowym AbstractListDetailFragment używa się NavHostFragment. Oznacza to, że możesz zdefiniować graf nawigacyjny zawierający tylko miejsca docelowe, które mają być wyświetlane w panelu szczegółów. Następnie możesz użyć przycisku NavController, aby przełączać panele szczegółów między miejscami docelowymi na niezależnym 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 w grafie nawigacji w panelu z informacjami nie mogą występować w żadnym zewnętrznym grafie nawigacji obejmującym całą aplikację. Jednak wszystkie precyzyjne linki w grafu nawigacji w panelu szczegółowym muszą być dołączone do miejsca docelowego, które hostuje SlidingPaneLayout. Dzięki temu zewnętrzne precyzyjne linki najpierw przekierowują do miejsca docelowego SlidingPaneLayout, a potem do odpowiedniego miejsca docelowego w panelu szczegółów.

Pełną implementację układu z 2 panelami za pomocą komponentu Nawigacja znajdziesz w przykładowym fragmencie TwoPaneFragment.

Integracja z systemowym przyciskiem Wstecz

Na mniejszych urządzeniach, na których panele listy i szczegółów zachodzą na siebie, upewnij się, że systemowy przycisk Wstecz przenosi użytkownika z panelu szczegółów z powrotem do panelu listy. Zapewnij możliwość korzystania z niestandardowej opcji Wstecz i połącz OnBackPressedCallback z bieżącym stanem 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);
    }
}

Możesz dodać wywołanie zwrotne do OnBackPressedDispatcher, używając: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 umożliwia ręczne wywoływanie open()close(), aby na telefonach przechodzić między listą a panelem szczegółów. Te metody nie mają żadnego wpływu, jeśli oba panele są widoczne i nie zachodzą na siebie.

Gdy panele listy i szczegółów zachodzą na siebie, użytkownicy mogą domyślnie przesuwać palcem w obu kierunkach, swobodnie przełączając się między tymi panelami nawet bez korzystania z nawigacji za pomocą gestów. Kierunek przesunięcia możesz kontrolować, ustawiając tryb blokady 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