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.
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 RecyclerView
powoduje 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.
Implementacja komponentu nawigacji
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()
i 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()
i 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: