Poruszanie się między fragmentami za pomocą animacji

Interfejs Fragment API udostępnia 2 sposoby używania efektów ruchu i przekształceń do wizualnego łączenia fragmentów podczas nawigacji. Jedną z nich jest platforma animacji, która korzysta zarówno z Animation, jak i Animator. Kolejnym elementem jest platforma przejścia, która obejmuje przejścia wspólne elementów.

Możesz określić niestandardowe efekty przy wpisywaniu i zamykaniu fragmentów oraz podczas przenoszenia elementów wspólnych między fragmentami.

  • Efekt wchodzenia określa sposób, w jaki fragment trafia na ekran. Możesz na przykład utworzyć efekt polegający na przesuwaniu fragmentu w stronę krawędzi ekranu, gdy przechodzisz do niego.
  • Efekt wyjścia określa sposób, w jaki fragment opuszcza ekran. Można na przykład utworzyć efekt zanikania przy opuszczaniu fragmentu.
  • Przejście z elementów wspólnych określa sposób przenoszenia między nimi widoku współdzielonego między 2 fragmentami. Na przykład obraz wyświetlany we fragmencie ImageView we fragmencie A przejdzie do fragmentu B, gdy stanie się widoczny.

ustawiania animacji,

Najpierw musisz utworzyć animacje dla efektów wejścia i wyjścia, które będą uruchamiane podczas przechodzenia do nowego fragmentu. Animacje możesz definiować jako dodatkowe zasoby animacji. Te zasoby pozwalają określić sposób rotacji, rozciągania, zanikania i przesuwania fragmentów podczas animacji. Na przykład bieżący fragment może zanikać i wysuwać się z prawej krawędzi ekranu, tak jak to widać na ilustracji 1.

Uruchamianie i zamykanie animacji. Bieżący fragment zanika, a następny fragment pojawia się z prawej strony.
Rysunek 1. Uruchamianie i zamykanie animacji. Bieżący fragment zanika, a następny fragment pojawia się po prawej stronie.

Te animacje można definiować w katalogu res/anim:

<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="1"
    android:toAlpha="0" />
<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="100%"
    android:toXDelta="0%" />

Możesz też określić animacje efektów wejścia i wyjścia, które są uruchamiane podczas tworzenia stosu wstecznego, co może mieć miejsce po kliknięciu przez użytkownika przycisku W górę lub Wstecz. Są to animacje popEnter i popExit. Na przykład, gdy użytkownik wróci do poprzedniego ekranu, możesz chcieć, aby bieżący fragment znikał z prawej krawędzi ekranu, a poprzedni fragment zanikał.

animacje popEnter i popExit. Bieżący fragment przesuwa się poza ekran w prawo, podczas gdy poprzedni fragment zanika.
Rysunek 2. Animacje: popEnter i popExit. Bieżący fragment przesuwa się poza ekran w prawo, a poprzedni zanika.

Animacje te można zdefiniować w ten sposób:

<!-- res/anim/slide_out.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="0%"
    android:toXDelta="100%" />
<!-- res/anim/fade_in.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="0"
    android:toAlpha="1" />

Gdy zdefiniujesz animacje, użyj ich, wywołując metodę FragmentTransaction.setCustomAnimations(), która przekazuje zasoby animacji według ich identyfikatora, jak w tym przykładzie:

Kotlin

supportFragmentManager.commit {
    setCustomAnimations(
        R.anim.slide_in, // enter
        R.anim.fade_out, // exit
        R.anim.fade_in, // popEnter
        R.anim.slide_out // popExit
    )
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(
        R.anim.slide_in,  // enter
        R.anim.fade_out,  // exit
        R.anim.fade_in,   // popEnter
        R.anim.slide_out  // popExit
    )
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

Konfigurowanie przejść

Możesz też używać przejść do definiowania efektów wejścia i wyjścia. Przeniesienia można określić w plikach zasobów XML. Na przykład bieżący fragment może zanikać i wysuwać się z prawej krawędzi ekranu. Te przejścia można zdefiniować w ten sposób:

<!-- res/transition/fade.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"/>
<!-- res/transition/slide_right.xml -->
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="right" />

Po zdefiniowaniu przejść zastosuj je, wywołując setEnterTransition() we fragmencie wprowadzającym i setExitTransition() we fragmencie wyjściowym, przekazując większe zasoby przejścia według ich identyfikatorów, jak w tym przykładzie:

Kotlin

class FragmentA : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }
}

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setExitTransition(inflater.inflateTransition(R.transition.fade));
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setEnterTransition(inflater.inflateTransition(R.transition.slide_right));
    }
}

Fragmenty obsługują przejścia na AndroidaX. Fragmenty obsługują również przejścia platform, ale zdecydowanie zalecamy korzystanie z przejścia AndroidX, ponieważ są one obsługiwane na poziomach API 14 i wyższych oraz zawierają poprawki błędów, których nie było w starszych wersjach przejść platformy.

Używanie przejść między elementami współdzielonymi

W ramach platformy przejścia przejścia wspólne elementów określają sposób, w jaki odpowiednie widoki poruszają się między 2 fragmentami podczas przejścia fragmentów. Może być np. tak, że obraz wyświetlany w elemencie ImageView we fragmencie A, aby został przeniesiony do fragmentu B, gdy stanie się widoczny, jak pokazano na ilustracji 3.

Przejście fragmentów ze wspólnym elementem.
Rysunek 3. Przejście fragmentów ze wspólnym elementem.

Ogólnie oto jak wykonać przejście fragmentu z elementami współdzielonymi:

  1. Do każdego widoku elementów współdzielonych przypisz unikalną nazwę przejścia.
  2. Dodaj do FragmentTransaction wspólne widoki elementów i nazwy przejść.
  3. Ustaw animację przejścia elementu wspólnego.

Po pierwsze, musisz przypisać do każdego widoku elementów wspólnych unikalną nazwę przejścia, aby umożliwić mapowanie widoków z jednego fragmentu do drugiego. Ustaw nazwę przejścia dla udostępnionych elementów w każdym układzie fragmentów za pomocą metody ViewCompat.setTransitionName(), która zapewnia zgodność z interfejsami API na poziomach 14 i wyższych. Na przykład nazwę przejścia dla elementu ImageView we fragmentach A i B można przypisać w ten sposób:

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val itemImageView = view.findViewById<ImageView>(R.id.item_image)
        ViewCompat.setTransitionName(itemImageView, “item_image”)
    }
}

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val heroImageView = view.findViewById<ImageView>(R.id.hero_image)
        ViewCompat.setTransitionName(heroImageView, “hero_image”)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView itemImageView = view.findViewById(R.id.item_image);
        ViewCompat.setTransitionName(itemImageView, “item_image”);
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView heroImageView = view.findViewById(R.id.hero_image);
        ViewCompat.setTransitionName(heroImageView, “hero_image”);
    }
}

Aby uwzględnić wspólne elementy w przejściu fragmentów, FragmentTransaction musi wiedzieć, jak widoki każdego elementu wspólnego mapują się z jednego fragmentu na drugi. Dodaj do FragmentTransaction wszystkie udostępnione elementy, wywołując FragmentTransaction.addSharedElement(), przekazując w następnym widoku i nazwę przejścia dla odpowiedniego widoku w następnym fragmencie, jak pokazano w tym przykładzie:

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setCustomAnimations(...)
    addSharedElement(itemImageView, “hero_image”)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(...)
    .addSharedElement(itemImageView, “hero_image”)
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

Aby określić sposób przechodzenia między elementami wspólnymi z jednego fragmentu do następnego, musisz ustawić przejście Enter na przechodzeniu do fragmentu. Wywołaj Fragment.setSharedElementEnterTransition() w metodzie onCreate() fragmentu, jak w tym przykładzie:

Kotlin

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = TransitionInflater.from(requireContext())
             .inflateTransition(R.transition.shared_image)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Transition transition = TransitionInflater.from(requireContext())
            .inflateTransition(R.transition.shared_image);
        setSharedElementEnterTransition(transition);
    }
}

Przejście shared_image jest zdefiniowane w ten sposób:

<!-- res/transition/shared_image.xml -->
<transitionSet>
    <changeImageTransform />
</transitionSet>

Wszystkie podklasy Transition są obsługiwane jako przejścia elementów wspólnych. Jeśli chcesz utworzyć niestandardową animację przejścia (Transition), przeczytaj sekcję Tworzenie niestandardowej animacji przejścia. Język changeImageTransform używany w poprzednim przykładzie jest jednym z dostępnych gotowych tłumaczeń, których możesz użyć. Dodatkowe podklasy Transition znajdziesz w dokumentacji interfejsu API klasy Transition.

Domyślnie wspólne przejście „Enter” jest też używane jako przejście powrót dla wspólnych elementów. Przejście zwrotne określa, w jaki sposób udostępnione elementy przechodzą z powrotem do poprzedniego fragmentu po wyjęciu transakcji fragmentowej ze stosu wstecznego. Jeśli chcesz określić inne przejście powrotne, możesz to zrobić za pomocą metody Fragment.setSharedElementReturnTransition() w metodzie onCreate() danego fragmentu.

Przewidywana zgodność wsteczna

Przewidywania wstecz możesz używać w przypadku wielu animacji z fragmentami krzyżowymi, ale nie ze wszystkimi. Podczas stosowania funkcji przewidywania wstecznej pamiętaj o tych kwestiach:

  • Zaimportuj wersję Transitions 1.5.0 lub nowszą i wersję Fragments 1.7.0 lub nowszą.
  • Obsługiwane są klasy i podklasy Animator oraz biblioteka przejścia na AndroidaX.
  • Biblioteka klas Animation i platformy Transition nie jest obsługiwana.
  • Animacje prognozowanych fragmentów działają tylko na urządzeniach z Androidem 14 lub nowszym.
  • Typy setCustomAnimations, setEnterTransition, setExitTransition, setReenterTransition, setReturnTransition, setSharedElementEnterTransition i setSharedElementReturnTransition są obsługiwane z funkcją przewidywania.

Więcej informacji znajdziesz w artykule o dodawaniu obsługi animacji przewidywanego przejścia wstecz.

Odkładanie migracji

W niektórych przypadkach może być konieczne opóźnienie przejścia fragmentu na krótki czas. Na przykład może być konieczne zaczekanie, aż wszystkie widoki we fragmencie wejścia zostaną zmierzone i ułożone, tak aby Android dokładnie rejestrował stan początkowy i końcowy przejścia.

Przenoszenie może też zostać odroczone, aż do wczytania niektórych niezbędnych danych. Być może trzeba na przykład poczekać na wczytanie obrazów dla elementów udostępnionych. W przeciwnym razie przejście może być problematyczne, jeśli zakończy się ładowanie obrazu w trakcie jego lub po jego zakończeniu.

Aby opóźnić przejście, najpierw musisz się upewnić, że transakcja fragmentu umożliwia zmianę kolejności zmian stanu fragmentu. Aby umożliwić zmianę kolejności zmian stanu fragmentu, wywołaj metodę FragmentTransaction.setReorderingAllowed(), jak pokazano w poniższym przykładzie:

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setReorderingAllowed(true)
    setCustomAnimation(...)
    addSharedElement(view, view.transitionName)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setReorderingAllowed(true)
    .setCustomAnimations(...)
    .addSharedElement(view, view.getTransitionName())
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

Aby opóźnić przejście do wprowadzania, wywołaj Fragment.postponeEnterTransition() w metodzie onViewCreated() wprowadzania we fragmencie:

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        postponeEnterTransition()
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        postponeEnterTransition();
    }
}

Gdy załadujesz dane i chcesz rozpocząć przenoszenie, wywołaj metodę Fragment.startPostponedEnterTransition(). Poniższy przykład korzysta z biblioteki Glide, aby wczytać obraz we współdzielonym elemencie ImageView, opóźniając odpowiednie przejście do momentu zakończenia wczytywania obrazu.

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        Glide.with(this)
            .load(url)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }

                override fun onResourceReady(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }
            })
            .into(headerImage)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        Glide.with(this)
            .load(url)
            .listener(new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(...) {
                    startPostponedEnterTransition();
                    return false;
                }

                @Override
                public boolean onResourceReady(...) {
                    startPostponedEnterTransition();
                    return false;
                }
            })
            .into(headerImage)
    }
}

Gdy masz do czynienia z powolnym połączeniem internetowym użytkownika, przenoszenie kont może zacząć się po upływie określonego czasu, a nie po zakończeniu ładowania wszystkich danych. W takich sytuacjach można zamiast tego wywołać metodę Fragment.postponeEnterTransition(long, TimeUnit) w metodzie onViewCreated() wpisywania, przekazując czas trwania i jednostkę czasu. Odroczenie rozpocznie się automatycznie po upływie określonego czasu.

Używanie przejść między elementami współdzielonymi za pomocą atrybutu RecyclerView

Opóźnione przejścia wejścia powinny zacząć się dopiero po zmierzeniu i ułożeniu wszystkich widoków we fragmencie wprowadzającym. Jeśli używasz RecyclerView, przed rozpoczęciem przenoszenia musisz poczekać, aż dane zostaną wczytane i że elementy RecyclerView będą gotowe do rysowania. Oto przykład:

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        postponeEnterTransition()

        // Wait for the data to load
        viewModel.data.observe(viewLifecycleOwner) {
            // Set the data on the RecyclerView adapter
            adapter.setData(it)
            // Start the transition once all views have been
            // measured and laid out
            (view.parent as? ViewGroup)?.doOnPreDraw {
                startPostponedEnterTransition()
            }
        }
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        postponeEnterTransition();

        final ViewGroup parentView = (ViewGroup) view.getParent();
        // Wait for the data to load
        viewModel.getData()
            .observe(getViewLifecycleOwner(), new Observer<List<String>>() {
                @Override
                public void onChanged(List<String> list) {
                    // Set the data on the RecyclerView adapter
                    adapter.setData(it);
                    // Start the transition once all views have been
                    // measured and laid out
                    parentView.getViewTreeObserver()
                        .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                            @Override
                            public boolean onPreDraw(){
                                parentView.getViewTreeObserver()
                                        .removeOnPreDrawListener(this);
                                startPostponedEnterTransition();
                                return true;
                            }
                    });
                }
        });
    }
}

Zwróć uwagę, że w widoku nadrzędnym widoku fragmentu jest ustawiony element ViewTreeObserver.OnPreDrawListener. Ma to na celu sprawdzenie, czy wszystkie widoki z fragmentu zostały zmierzone i umieszczone, a tym samym gotowe do pobrania przed rozpoczęciem opóźnionego przejścia do wejścia.

Przy korzystaniu z przejścia elementów wspólnych z elementem RecyclerView nie można też ustawić nazwy przejścia w układzie XML elementu RecyclerView, ponieważ dowolna liczba elementów korzysta z tego układu. Aby animacja przejścia korzystała z prawidłowego widoku, musisz przypisać unikalną nazwę przejścia.

Możesz nadać wspólnemu elementowi każdego elementu niepowtarzalną nazwę przejścia. W tym celu przypisz je, gdy powiązany jest parametr ViewHolder. Jeśli np. dane poszczególnych elementów zawierają niepowtarzalny identyfikator, możesz go użyć jako nazwy przejścia, jak w tym przykładzie:

Kotlin

class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val image = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
        ViewCompat.setTransitionName(image, id)
        ...
    }
}

Java

public class ExampleViewHolder extends RecyclerView.ViewHolder {
    private final ImageView image;

    ExampleViewHolder(View itemView) {
        super(itemView);
        image = itemView.findViewById(R.id.item_image);
    }

    public void bind(String id) {
        ViewCompat.setTransitionName(image, id);
        ...
    }
}

Dodatkowe materiały

Więcej informacji o przejściach fragmentów znajdziesz poniżej, w dodatkowych materiałach.

Próbki

Posty na blogu