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ść:
Groovy
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.
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.
Implementacja komponentu nawigacji
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: