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 sekcji Utwórz

Każdy ekran aplikacji musi reagować i dostosowywać się do dostępnego miejsca. Możesz utworzyć elastyczny interfejs za pomocą ConstraintLayout, który umożliwia skalowanie metody z jednym panelem do wielu rozmiarów, ale podzielenie układu na wiele paneli może być korzystne dla większych urządzeń. Może to być na przykład ekran z listą elementów obok listy z informacjami o wybranym elemencie.

Komponent SlidingPaneLayout umożliwia wyświetlanie 2 paneli obok siebie na większych urządzeniach i urządzeniach składanych oraz automatycznie dostosowuje się do wyświetlania tylko jednego panelu naraz na mniejszych urządzeniach, np. telefonach.

Wskazówki dotyczące konkretnych urządzeń znajdziesz w omówieniu zgodności ekranu.

Skonfiguruj

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

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 układ z 2 panelami do użytku na najwyższym poziomie interfejsu. Ten układ wykorzystuje pierwszy panel jako listę treści lub przeglądarkę. Podrzędny jest główny widok szczegółów w celu wyświetlania treści w drugim panelu.

Obraz pokazujący przykładowy układ panelu przesuwnego
Rysunek 1. Przykład układu utworzonego za pomocą właściwości SlidingPaneLayout.

SlidingPaneLayout określa, czy wyświetlić je obok siebie, na podstawie szerokości obu paneli. Jeśli na przykład wiemy, że panel listy ma minimalny rozmiar 200 dp, a panel szczegółów – 400 dp, SlidingPaneLayout automatycznie wyświetla 2 panele obok siebie, o ile mają one szerokość co najmniej 600 dp.

Widoki podrzędne nakładają się, jeśli ich łączna szerokość przekracza dostępną szerokość kolumny SlidingPaneLayout. W takim przypadku widoki podrzędne rozwijają się, wypełniając całą szerokość dostępną w elemencie SlidingPaneLayout. Użytkownik może przesunąć widok najwyżej, przeciągając go z powrotem za krawędź ekranu.

Jeśli widoki się nie nakładają, SlidingPaneLayout umożliwia użycie w widokach podrzędnych parametru układu layout_weight, aby określić, jak podzielić pozostałą przestrzeń po zakończeniu pomiaru. Ten parametr dotyczy tylko szerokości.

Na urządzeniu składanym, które ma miejsce na ekranie do wyświetlania obu widoków obok siebie, SlidingPaneLayout automatycznie dostosowuje rozmiar dwóch paneli, aby były one umieszczone po obu stronach nakładających się lub zawiasów. W tym przypadku ustawione szerokości są uznawane za minimalną szerokość, która musi istnieć po każdej stronie elementu zwijania. Jeśli nie ma wystarczającej ilości miejsca, aby zachować minimalny rozmiar, SlidingPaneLayout przełącza się z powrotem na nakładające się widoki.

Oto przykład użycia elementu SlidingPaneLayout, który ma RecyclerView jako lewy panel i FragmentContainerView jako główny widok szczegółów do wyświetlania treści z panelu po lewej stronie:

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

Zautomatyzowana zamiana panelu szczegółów

W poprzednim przykładzie XML kliknięcie elementu w elemencie RecyclerView wywołuje zmianę w panelu szczegółów. W przypadku używania fragmentów wymaga to elementu FragmentTransaction, który zastępuje panel po prawej stronie i wywołuje w tabeli SlidingPaneLayout metodę open(), która przemienia się 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.

Przykłady na tej stronie korzystają bezpośrednio z metody SlidingPaneLayout i wymagają ręcznego zarządzania transakcjami związanymi z fragmentami. Jednak komponent Nawigacja udostępnia gotową implementację układu z 2 panelami za pomocą AbstractListDetailFragment – klasy API, która do zarządzania listami i panelami szczegółów korzysta z SlidingPaneLayout.

Pozwala to uprościć konfigurację układu XML. Zamiast jawnie zadeklarować panel SlidingPaneLayout i obydwa panele, układ wymaga tylko elementu 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>

Aby zapewnić niestandardowy widok panelu listy, zaimplementuj onCreateListPaneView() i onListPaneViewCreated(). W panelu szczegółów AbstractListDetailFragment używa klasy NavHostFragment. Oznacza to, że możesz zdefiniować wykres nawigacyjny zawierający tylko miejsca docelowe widoczne w panelu szczegółów. Następnie za pomocą narzędzia NavController możesz przełączać się między miejscami docelowymi na samodzielnym wykresie nawigacyjnym.

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 na wykresie nawigacyjnym panelu szczegółów nie mogą występować na zewnętrznym wykresie nawigacyjnym dotyczącym całej aplikacji. Jednak wszystkie precyzyjne linki na wykresie nawigacyjnym w panelu ze szczegółami muszą być dołączone do miejsca docelowego, w którym hostowany jest element SlidingPaneLayout. Dzięki temu zewnętrzne precyzyjne linki najpierw przechodzą do miejsca docelowego SlidingPaneLayout, a następnie do właściwego miejsca docelowego panelu szczegółów.

W przykładzie TwoPaneFragment znajdziesz pełną implementację układu z 2 panelami za pomocą komponentu Nawigacja.

Integracja z systemowym przyciskiem Wstecz

Na mniejszych urządzeniach, których panele listy i szczegóły nakładają się na siebie, dopilnuj, aby systemowy przycisk Wstecz przenosi użytkownika z panelu szczegółów z powrotem do panelu listy. Aby to zrobić, udostępnij niestandardową nawigację wsteczną i połącz OnBackPressedCallback z bieżącym stanem elementu 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 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 zawsze umożliwia ręczne wywoływanie funkcji open() i close() w celu przejścia między panelami listy i szczegółów na telefonie. Te metody 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ę, użytkownicy mogą domyślnie przesuwać palcem w obu kierunkach, swobodnie przełączać się między nimi, nawet jeśli nie używają nawigacji przy użyciu gestów. Kierunek przesuwania możesz sterować, 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 różne formaty znajdziesz w tej dokumentacji:

Dodatkowe materiały