Migracja z ViewPager do ViewPager2

ViewPager2 to ulepszona wersja biblioteki ViewPager, która oferuje rozszerzone funkcje i rozwiązuje typowe problemy związane z używaniem ViewPager. Jeśli Twoja aplikacja korzysta już z ViewPager, przeczytaj tę stronę, aby dowiedzieć się więcej o migracji do ViewPager2.

Jeśli chcesz używać ViewPager2 w aplikacji, a obecnie nie korzystasz z ViewPager, przeczytaj artykuły Slide between fragments using ViewPager2 (Przesuwanie między fragmentami za pomocą ViewPager2) i Create swipe views with tabs using ViewPager2 (Tworzenie widoków przesuwania z kartami za pomocą ViewPager2), aby dowiedzieć się więcej.

Zalety przejścia na ViewPager2

Głównym powodem migracji jest to, że ViewPager2 jest aktywnie rozwijany, a ViewPager nie. Jednak ViewPager2 oferuje też kilka innych, bardziej szczegółowych korzyści.

Obsługa orientacji pionowej

ViewPager2 obsługuje stronicowanie w pionie, a także tradycyjne stronicowanie w poziomie. Możesz włączyć stronicowanie pionowe dla elementu ViewPager2, ustawiając jego atrybut android:orientation:

<androidx.viewpager2.widget.ViewPager2
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:orientation="vertical" />

Możesz też ustawić ten atrybut automatycznie, używając metody setOrientation().

Obsługa języków pisanych od prawej do lewej

ViewPager2 obsługuje stronicowanie od prawej do lewej. Stronicowanie od prawej do lewej jest włączane automatycznie, gdy jest to odpowiednie dla danego języka, ale możesz też ręcznie włączyć stronicowanie od prawej do lewej dla elementu ViewPager2, ustawiając jego atrybut android:layoutDirection:

<androidx.viewpager2.widget.ViewPager2
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layoutDirection="rtl" />

Możesz też ustawić ten atrybut automatycznie, używając metody setLayoutDirection().

Kolekcje fragmentów, które można modyfikować

ViewPager2 obsługuje stronicowanie w modyfikowalnej kolekcji fragmentów, wywołując notifyDatasetChanged() w celu zaktualizowania interfejsu, gdy zmieni się kolekcja bazowa.

Oznacza to, że aplikacja może dynamicznie modyfikować kolekcję fragmentów w czasie działania, a ViewPager2 będzie prawidłowo wyświetlać zmodyfikowaną kolekcję.

DiffUtil

ViewPager2 jest oparty na RecyclerView, co oznacza, że ma dostęp do klasy użytkowej DiffUtil. Daje to kilka korzyści, ale przede wszystkim oznacza, że obiekty ViewPager2 natywnie wykorzystują animacje zmiany zbioru danych z klasy RecyclerView.

Przenoszenie aplikacji do ViewPager2

Aby zaktualizować obiekty ViewPager w aplikacji do wersji ViewPager2, wykonaj te czynności:

Aktualizowanie plików układu XML

Najpierw zastąp elementy ViewPager w plikach układu XML elementami ViewPager2:

<!-- A ViewPager element -->
<android.support.v4.view.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<!-- A ViewPager2 element -->
<androidx.viewpager2.widget.ViewPager2
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Aktualizowanie klas adapterów

W przypadku ViewPager trzeba było rozszerzyć klasę adaptera, która dostarczała nowe strony do obiektu. W zależności od przypadku użycia firma ViewPager korzystała z 3 różnych klas abstrakcyjnych. ViewPager2 używa tylko 2 klas abstrakcyjnych.

W przypadku każdego obiektu ViewPager, który konwertujesz na obiekt ViewPager2, zaktualizuj klasę adaptera, aby rozszerzyć odpowiednią klasę abstrakcyjną w ten sposób:

Parametry konstruktora

Klasy adapterów oparte na fragmentach, które dziedziczą z FragmentPagerAdapter lub FragmentStatePagerAdapter, zawsze akceptują pojedynczy obiekt FragmentManager jako parametr konstruktora. Gdy rozszerzasz klasę FragmentStateAdapter na potrzeby klasy adaptera ViewPager2, masz do wyboru te opcje parametrów konstruktora:

  • Obiekt FragmentActivity lub Fragment, w którym znajduje się obiekt ViewPager2. W większości przypadków jest to lepsze rozwiązanie.
  • Obiekt FragmentManager i obiekt Lifecycle.

Klasy adapterów oparte na widoku, które dziedziczą bezpośrednio z RecyclerView.Adapter, nie wymagają parametru konstruktora.

Metody zastępowania

Klasy adaptera muszą też zastępować inne metody w przypadku ViewPager2 niż w przypadku ViewPager:

  • Zamiast getCount() zastąp getItemCount(). Oprócz nazwy ta metoda nie uległa zmianie.
  • Zamiast getItem() zastąp createFragment() w klasach adaptera opartych na fragmentach. Upewnij się, że metoda createFragment() zawsze dostarcza nową instancję fragmentu przy każdym wywołaniu funkcji, zamiast ponownie używać instancji.

Podsumowanie

Podsumowując, aby przekonwertować klasę adaptera ViewPager do użycia z ViewPager2, musisz wprowadzić te zmiany:

  1. Zmień klasę nadrzędną na RecyclerView.Adapter, aby stronicować widoki, lub na FragmentStateAdapter, aby stronicować fragmenty.
  2. Zmień parametry konstruktora w klasach adapterów opartych na fragmentach.
  3. Zastąp getItemCount() zamiast getCount().
  4. Zastąp createFragment() zamiast getItem() w klasach adaptera opartych na fragmentach.

Kotlin

// A simple ViewPager adapter class for paging through fragments
class ScreenSlidePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
    override fun getCount(): Int = NUM_PAGES

    override fun getItem(position: Int): Fragment = ScreenSlidePageFragment()
}

// An equivalent ViewPager2 adapter class
class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
    override fun getItemCount(): Int = NUM_PAGES

    override fun createFragment(position: Int): Fragment = ScreenSlidePageFragment()
}

Java

// A simple ViewPager adapter class for paging through fragments
public class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
    public ScreenSlidePagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        return new ScreenSlidePageFragment();
    }

    @Override
    public int getCount() {
        return NUM_PAGES;
    }
}

// An equivalent ViewPager2 adapter class
private class ScreenSlidePagerAdapter extends FragmentStateAdapter {
    public ScreenSlidePagerAdapter(FragmentActivity fa) {
        super(fa);
    }

    @Override
    public Fragment createFragment(int position) {
        return new ScreenSlidePageFragment();
    }

    @Override
    public int getItemCount() {
        return NUM_PAGES;
    }
}

Refaktoryzacja interfejsów TabLayout

ViewPager2 wprowadza zmiany w integracji z TabLayout. Jeśli obecnie używasz ViewPager z obiektem TabLayout do wyświetlania poziomych kart nawigacyjnych, musisz refaktoryzować obiekt TabLayout, aby zintegrować go z ViewPager2.

Komponent TabLayout został odłączony od ViewPager2 i jest teraz dostępny w ramach komponentów Material Design. Oznacza to, że aby go używać, musisz dodać odpowiednią zależność do pliku build.gradle:

Dynamiczny

implementation "com.google.android.material:material:1.1.0-beta01"

Kotlin

implementation("com.google.android.material:material:1.1.0-beta01")

Musisz też zmienić lokalizację elementu TabLayout w hierarchii pliku układu XML. W przypadku ViewPager element TabLayout jest deklarowany jako element podrzędny elementu ViewPager, ale w przypadku ViewPager2 element TabLayout jest deklarowany bezpośrednio nad elementem ViewPager2, na tym samym poziomie:

<!-- A ViewPager element with a TabLayout -->
<androidx.viewpager.widget.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</androidx.viewpager.widget.ViewPager>

<!-- A ViewPager2 element with a TabLayout -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>

Na koniec musisz zaktualizować kod, który dołącza obiekt TabLayout do obiektu ViewPager. TabLayout używa własnej metody setupWithViewPager() do integracji z ViewPager, ale do integracji z ViewPager2 wymaga instancji TabLayoutMediator.

Obiekt TabLayoutMediator obsługuje też zadanie generowania tytułów stron dla obiektu TabLayout, co oznacza, że klasa adaptera nie musi zastępować metody getPageTitle():

Kotlin

// Integrating TabLayout with ViewPager
class CollectionDemoFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val tabLayout = view.findViewById(R.id.tab_layout)
        tabLayout.setupWithViewPager(viewPager)
    }
    ...
}

class DemoCollectionPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {

    override fun getCount(): Int  = 4

    override fun getPageTitle(position: Int): CharSequence {
        return "OBJECT ${(position + 1)}"
    }
    ...
}

// Integrating TabLayout with ViewPager2
class CollectionDemoFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val tabLayout = view.findViewById(R.id.tab_layout)
        TabLayoutMediator(tabLayout, viewPager) { tab, position ->
            tab.text = "OBJECT ${(position + 1)}"
        }.attach()
    }
    ...
}

Java

// Integrating TabLayout with ViewPager
public class CollectionDemoFragment extends Fragment {
    ...
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        TabLayout tabLayout = view.findViewById(R.id.tab_layout);
        tabLayout.setupWithViewPager(viewPager);
    }
    ...
}

public class DemoCollectionPagerAdapter extends FragmentStatePagerAdapter {
    ...
    @Override
    public int getCount() {
        return 4;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return "OBJECT " + (position + 1);
    }
    ...
}

// Integrating TabLayout with ViewPager2
public class CollectionDemoFragment : Fragment() {
    ...
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        TabLayout tabLayout = view.findViewById(R.id.tab_layout);
        new TabLayoutMediator(tabLayout, viewPager,
                (tab, position) -> tab.setText("OBJECT " + (position + 1))
        ).attach();
    }
    ...
}

Obsługa zagnieżdżonych elementów przewijanych

ViewPager2 nie obsługuje natywnie zagnieżdżonych widoków przewijania w przypadkach, gdy widok przewijania ma taką samą orientację jak obiekt ViewPager2, który go zawiera. Na przykład przewijanie nie będzie działać w przypadku widoku przewijania pionowego wewnątrz obiektu ViewPager2 o orientacji pionowej.

Aby obsługiwać widok przewijania w obiekcie ViewPager2 o tej samej orientacji, musisz wywołać requestDisallowInterceptTouchEvent() na obiekcie ViewPager2, gdy chcesz przewijać zagnieżdżony element. Przykładowe zagnieżdżone przewijanie ViewPager2 pokazuje, jak rozwiązać ten problem za pomocą uniwersalnego niestandardowego układu opakowania.

Dodatkowe materiały

Więcej informacji o ViewPager2 znajdziesz w tych materiałach.

Filmy