Każdy ekran w aplikacji musi być elastyczny i dostosowywać się do dostępnego miejsca.
Możesz utworzyć elastyczny interfejsConstraintLayout, który umożliwi skalowanie układu z jednym panelem do wielu rozmiarów, ale w przypadku większych urządzeń korzystniejsze może być podzielenie układu na kilka paneli. Na przykład możesz chcieć, aby na ekranie wyświetlała się lista 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 się do wyświetlania tylko jednego panelu naraz na mniejszych urządzeniach, takich jak telefony.
Wskazówki dotyczące konkretnych urządzeń znajdziesz w omówieniu zgodności ekranów.
Konfiguracja
Aby używać SlidingPaneLayout, dodaj tę zależność do pliku build.gradle aplikacji:
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 dwupanelowy do użycia na najwyższym poziomie interfejsu. W tym układzie pierwszy panel służy jako lista treści lub przeglądarka, która jest podrzędna względem głównego widoku szczegółów wyświetlającego treści w drugim panelu.
SlidingPaneLayout.
SlidingPaneLayout określa, czy wyświetlać panele obok siebie, na podstawie szerokości obu paneli. Jeśli na przykład panel listy ma minimalny rozmiar 200 dp, a panel szczegółów potrzebuje 400 dp, SlidingPaneLayout automatycznie wyświetli oba panele obok siebie, o ile ma do dyspozycji co najmniej 600 dp szerokości.
Widoki podrzędne nakładają się, jeśli ich łączna szerokość przekracza dostępną szerokość w SlidingPaneLayout. W tym przypadku widoki dziecka rozszerzają się, aby wypełnić dostępną szerokość na urządzeniu SlidingPaneLayout. Użytkownik może przesunąć widok na wierzchu, przeciągając go z powrotem od krawędzi ekranu.
Jeśli widoki nie nakładają się na siebie, SlidingPaneLayout obsługuje parametr układu layout_weight w przypadku widoków podrzędnych, aby określić, jak podzielić pozostałe miejsce po zakończeniu pomiaru. Ten parametr jest istotny tylko w przypadku szerokości.
Na urządzeniu składanym, na którym jest miejsce na ekranie, aby wyświetlać oba widoki obok siebie, SlidingPaneLayout automatycznie dostosowuje rozmiar obu paneli, tak aby były one umieszczone po obu stronach zakładki lub zawiasu. W tym przypadku ustawione szerokości są traktowane jako minimalna szerokość, która musi występować po każdej stronie elementu składanego. Jeśli nie ma wystarczająco dużo miejsca, aby zachować ten minimalny rozmiar, SlidingPaneLayout wraca do nakładania się widoków.
Oto przykład użycia elementu SlidingPaneLayout, który ma RecyclerView jako panel po lewej stronie 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 po pierwszym uruchomieniu aplikacji.
Programowe zastępowanie panelu szczegółów
W przykładzie XML powyżej kliknięcie elementu w RecyclerView
spowoduje zmianę w panelu szczegółów. W przypadku fragmentów wymaga to FragmentTransaction, które zastępuje prawy panel, wywołując open() w SlidingPaneLayout, aby przełączyć 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 funkcji addToBackStack() w FragmentTransaction. Zapobiega to tworzeniu stosu wstecznego w panelu szczegółów.
Implementacja komponentu Navigation
Przykłady na tej stronie używają bezpośrednio SlidingPaneLayout i wymagają ręcznego zarządzania transakcjami fragmentów. Jednak komponent Navigation udostępnia gotową implementację układu dwupanelowego za pomocą AbstractListDetailFragment, klasy interfejsu API, która pod maską używa SlidingPaneLayout do zarządzania panelami listy i szczegółów.
Ułatwia to konfigurację układu XML. Zamiast jawnie deklarować SlidingPaneLayout i oba panele, 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>
Zaimplementuj
onCreateListPaneView()
i
onListPaneViewCreated()
, aby zapewnić niestandardowy widok panelu listy. W panelu szczegółów AbstractListDetailFragment używa NavHostFragment.
Oznacza to, że możesz zdefiniować graf nawigacji, który zawiera tylko miejsca docelowe, które mają być wyświetlane w panelu szczegółów. Następnie możesz użyć
NavController, aby przełączać
panel szczegółów między miejscami docelowymi w samodzielnym 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 szczegółów nie mogą występować w żadnym zewnętrznym grafie nawigacji w aplikacji. Wszystkie precyzyjne linki w grafie nawigacji panelu szczegółów muszą być jednak dołączone do miejsca docelowego, w którym znajduje się SlidingPaneLayout. Dzięki temu zewnętrzne precyzyjne linki najpierw prowadzą do miejsca docelowego SlidingPaneLayout, a potem do odpowiedniego panelu szczegółów.
Pełną implementację układu dwupanelowego za pomocą komponentu Navigation 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, upewnij się, że przycisk Wstecz w systemie przenosi użytkownika z panelu szczegółów z powrotem do panelu listy. W tym celu zapewnij niestandardową nawigację 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 elementu
OnBackPressedDispatcher
za pomocą elementu
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łanie open() i close()
w celu przełączania się między listą a panelem szczegółów na telefonach. Te metody nie mają wpływu na widoczność, 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ć je w obu kierunkach, swobodnie przełączając się między nimi nawet wtedy, gdy nie korzystają z nawigacji gestami. 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 różne formaty znajdziesz w tych dokumentach: