Как переключаться между фрагментами с помощью анимации

Fragment API предоставляет два способа использования эффектов движения и преобразований для визуального соединения фрагментов во время навигации. Один из них – Animation Framework, который использует и Animation, и Animator. Второй – Transition Framework, который включает переходы между общими элементами.

Вы можете указать собственные эффекты для входа и выхода из фрагментов, а также для переходов общих элементов между фрагментами.

  • Эффект входа определяет, как фрагмент появляется на экране. Например, вы можете создать эффект, чтобы фрагмент выдвигался от края экрана, когда вы переходите к нему.
  • Эффект выхода определяет, как фрагмент исчезает с экрана. Например, вы можете создать эффект затухания фрагмента при переходе от него.
  • Переход общего элемента определяет, как представление, общее для двух фрагментов, перемещается между ними. Например, изображение, отображаемое в ImageView во фрагменте A, переходит во фрагмент B, как только B становится видимым.

включать и выключать анимацию реакций;

Сначала вам нужно создать анимацию для эффектов входа и выхода, которые запускаются при переходе к новому фрагменту. Вы можете определить анимацию как ресурсы анимации перехода. Эти ресурсы позволяют определить, как фрагменты должны вращаться, растягиваться, исчезать и перемещаться во время анимации. Например, вы можете захотеть, чтобы текущий фрагмент исчезал, а новый фрагмент скользил от правого края экрана, как показано на рисунке 1.

Анимация при появлении и исчезновении. Текущий фрагмент исчезает, а следующий появляется справа.
Рисунок 1. Анимация при появлении и исчезновении. Текущий фрагмент исчезает, а следующий появляется справа.

Эти анимации можно определить в каталоге 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%" />

Вы также можете указать анимацию для эффектов входа и выхода, которые запускаются при извлечении обратного стека, что может произойти, когда пользователь нажимает кнопку «Вверх» или кнопку «Назад». Они называются popEnter и popExit. Например, когда пользователь возвращается к предыдущему экрану, текущий фрагмент может сдвигаться за правый край экрана, а предыдущий – появляться.

анимации popEnter и popExit. Текущий фрагмент сдвигается вправо, а предыдущий появляется на экране.
Рисунок 2. popEnter и popExit. Текущий фрагмент сдвигается вправо за пределы экрана, а предыдущий фрагмент появляется с эффектом затухания.

Эти анимации можно определить следующим образом:

<!-- 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" />

Определив анимации, используйте их, вызвав метод FragmentTransaction.setCustomAnimations() и передав ему ресурсы анимации по их идентификаторам, как показано в следующем примере:

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();

Настроить переходы

Вы также можете использовать переходы, чтобы задать эффекты появления и исчезновения. Эти переходы можно определить в файлах ресурсов XML. Например, вы можете захотеть, чтобы текущий фрагмент исчезал, а новый фрагмент скользил от правого края экрана. Эти переходы можно определить следующим образом:

<!-- 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" />

Определив переходы, примените их, вызвав метод setEnterTransition() для входящего фрагмента и метод setExitTransition() для исходящего фрагмента. При этом передайте ресурсы перехода по их идентификаторам, как показано в примере ниже.

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));
    }
}

Фрагменты поддерживают переходы AndroidX. Хотя фрагменты также поддерживают переходы фреймворка, мы настоятельно рекомендуем использовать переходы AndroidX, поскольку они поддерживаются в API уровня 14 и выше и содержат исправления ошибок, которых нет в более старых версиях переходов фреймворка.

Используйте переходы между общими элементами

Переходы общих элементов, являющиеся частью Transition Framework, определяют, как соответствующие представления перемещаются между двумя фрагментами во время перехода фрагмента. Например, вы можете захотеть, чтобы изображение, отображаемое в ImageView во фрагменте A, переходило во фрагмент B, как только B станет видимым, как показано на рисунке 3.

Переход фрагмента с общим элементом.
Рисунок 3. Переход фрагмента с общим элементом.

Вот как сделать переход фрагмента с общими элементами:

  1. Назначьте уникальное название перехода для каждого общего элемента.
  2. Добавьте общие представления элементов и названия переходов в FragmentTransaction.
  3. Настройте анимацию перехода для общего элемента.

Сначала необходимо назначить уникальное имя перехода для каждого общего элемента, чтобы сопоставить представления между фрагментами. Задайте название перехода для общих элементов в каждом макете фрагмента с помощью ViewCompat.setTransitionName(), который обеспечивает совместимость с уровнем API 14 и выше. Например, название перехода для ImageView во фрагментах A и 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”);
    }
}

Чтобы включить общие элементы в переход фрагмента, ваш FragmentTransaction должен знать, как представления каждого общего элемента сопоставляются от одного фрагмента к другому. Добавьте каждый из общих элементов в FragmentTransaction, вызвав FragmentTransaction.addSharedElement() и передав представление и название перехода соответствующего представления в следующем фрагменте, как показано в примере ниже.

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();

Чтобы указать, как общие элементы переходят от одного фрагмента к другому, необходимо задать переход enter для фрагмента, к которому осуществляется переход. Вызовите метод Fragment.setSharedElementEnterTransition() в методе onCreate() фрагмента, как показано в примере ниже.

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);
    }
}

Переход shared_image определяется следующим образом:

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

Все подклассы Transition поддерживаются как переходы общих элементов. Если вы хотите создать собственный класс Transition, ознакомьтесь с разделом Как создать собственную анимацию перехода. changeImageTransform, использованный в предыдущем примере, – один из доступных встроенных переводов. Дополнительные подклассы Transition можно найти в документации по API для класса Transition.

По умолчанию переход входа общего элемента также используется в качестве перехода возврата для общих элементов. Переход возврата определяет, как общие элементы возвращаются к предыдущему фрагменту, когда транзакция фрагмента извлекается из обратного стека. Если вы хотите указать другой переход возврата, вы можете сделать это с помощью Fragment.setSharedElementReturnTransition() в методе onCreate() фрагмента.

Совместимость с предпросмотром для жеста "Назад"

Предпросмотр для жеста "Назад" можно использовать со многими, но не со всеми анимациями перехода между фрагментами. При реализации предпросмотра для жеста "Назад" учитывайте следующее:

  • Импортируйте Transitions 1.5.0 или более позднюю версию и Fragments 1.7.0 или более позднюю версию.
  • Поддерживаются класс Animator и его подклассы, а также библиотека AndroidX Transition.
  • Класс Animation и библиотека фреймворка Transition не поддерживаются.
  • Анимация прогнозируемых фрагментов работает только на устройствах под управлением Android 14 или выше.
  • Предпросмотр для жеста "Назад" поддерживается в следующих компонентах: setCustomAnimations, setEnterTransition, setExitTransition, setReenterTransition, setReturnTransition, setSharedElementEnterTransition и setSharedElementReturnTransition.

Подробнее о том, как добавить поддержку анимаций с предпросмотром для жеста "Назад"…

Откладывание переходов

В некоторых случаях может потребоваться отложить переход фрагмента на короткий период времени. Например, вам может потребоваться подождать, пока все представления во входящем фрагменте будут измерены и размещены, чтобы Android мог точно зафиксировать их начальное и конечное состояния для перехода.

Кроме того, переход может быть отложен до загрузки необходимых данных. Например, вам может потребоваться дождаться загрузки изображений для общих элементов. В противном случае переход может быть резким, если изображение завершит загрузку во время или после перехода.

Чтобы отложить переход, сначала убедитесь, что транзакция фрагмента позволяет изменять порядок изменений состояния фрагмента. Чтобы разрешить изменение порядка фрагментов, вызовите FragmentTransaction.setReorderingAllowed(), как показано в примере ниже.

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();

Чтобы отложить переход, вызовите метод Fragment.postponeEnterTransition() в методе onViewCreated() фрагмента, в который выполняется переход:

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();
    }
}

После загрузки данных и подготовки к переходу вызовите Fragment.startPostponedEnterTransition(). В примере ниже используется библиотека Glide, которая загружает изображение в общий элемент ImageView и откладывает переход до завершения загрузки.

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)
    }
}

При работе с такими случаями, как медленное подключение пользователя к Интернету, вам может потребоваться отложенный переход, чтобы начать его через определенное время, а не ждать загрузки всех данных. В таких случаях можно вызвать метод Fragment.postponeEnterTransition(long, TimeUnit) в методе onViewCreated() фрагмента входа, передав длительность и единицу времени. По истечении указанного времени обновление начнется автоматически.

Как использовать переходы с общими элементами в RecyclerView

Отложенные переходы не должны начинаться, пока не будут измерены и размещены все представления во фрагменте. При использовании RecyclerView перед началом перехода необходимо дождаться загрузки всех данных и готовности элементов RecyclerView к отрисовке. Рассмотрим пример.

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;
                            }
                    });
                }
        });
    }
}

Обратите внимание, что для родительского элемента фрагмента задан атрибут ViewTreeObserver.OnPreDrawListener. Это необходимо, чтобы все представления фрагмента были измерены и размещены и, следовательно, готовы к отрисовке до начала отложенного перехода входа.

Ещё один момент, который следует учитывать при использовании переходов общих элементов с RecyclerView, заключается в том, что вы не можете задать имя перехода в XML-макете элемента RecyclerView, поскольку этот макет использует произвольное количество элементов. Чтобы в анимации перехода использовалось правильное представление, необходимо назначить уникальное название перехода.

Вы можете присвоить каждому общему элементу уникальное имя перехода, назначив его при привязке ViewHolder. Например, если данные для каждого элемента включают уникальный идентификатор, его можно использовать в качестве имени перехода, как показано в следующем примере:

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);
        ...
    }
}

Дополнительные ресурсы

Дополнительную информацию о переходах между фрагментами можно найти в следующих ресурсах.

Образцы

Записи в блоге