Перемещайтесь между фрагментами с помощью анимации

API-интерфейс Fragment предоставляет два способа использования эффектов движения и преобразований для визуального соединения фрагментов во время навигации. Одним из них является 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() , передав ресурсы анимации по их идентификатору ресурса, как показано в следующем примере:

Котлин

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

Ява

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() для выходного фрагмента, передав завышенные ресурсы перехода по их идентификатору ресурса, как показано в следующем примере:

Котлин

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

Ява

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 можно назначить следующим образом:

Котлин

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

Ява

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() , передав представление и имя перехода соответствующего представления в следующем фрагменте, как показано в следующем примере:

Котлин

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

Ява

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

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

Котлин

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

Ява

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() , как показано в следующем примере:

Котлин

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

Ява

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() входящего фрагмента:

Котлин

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

Ява

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

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

Котлин

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

Ява

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 к отрисовке. Вот пример:

Котлин

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

Ява

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 . Например, если данные для каждого элемента содержат уникальный идентификатор, его можно использовать в качестве имени перехода, как показано в следующем примере:

Котлин

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

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

Ява

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

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

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

Образцы

Сообщения в блоге