使用动画在片段之间导航

Fragment API 提供了两种方法来使用运动效果和转换在导航期间视觉连接片段。其中之一是动画框架,它同时使用 AnimationAnimator。另一种是 转换框架,其中包括共享元素转换。

您可以为进入和退出片段以及片段之间共享元素的转换指定自定义效果。

  • 进入效果确定片段如何进入屏幕。例如,您可以在导航到片段时创建一种效果,使片段从屏幕边缘滑入。
  • 退出效果确定片段如何退出屏幕。例如,您可以在导航离开片段时创建一种效果,使片段淡出。
  • 共享元素转换确定在两个片段之间共享的视图如何在它们之间移动。例如,在片段 A 的 ImageView 中显示的图像在 B 可见后会过渡到片段 B。

设置动画

首先,您需要为进入和退出效果创建动画,这些动画在导航到新片段时运行。您可以将动画定义为 补间动画资源。这些资源允许您定义片段在动画期间应如何旋转、拉伸、淡入淡出和移动。例如,您可能希望当前片段淡出,而新片段从屏幕右侧滑入,如图 1 所示。

Enter and exit animations. The current fragment fades out while
            the next fragment slides in from the right.
图 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%" />

您还可以为弹出返回栈时运行的进入和退出效果指定动画,这可能发生在用户点击向上或后退按钮时。这些被称为popEnterpopExit动画。例如,当用户返回到上一个屏幕时,您可能希望当前片段从屏幕右侧滑出,而上一个片段淡入。

popEnter and popExit animations. The current fragment slides off
            the screen to the right while the previous fragment fades in.
图 2. popEnterpopExit动画。当前片段从屏幕右侧滑出,而上一个片段淡入。

这些动画可以按如下方式定义

<!-- 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()并传入动画资源的资源 ID 来使用它们,如下例所示

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()来应用它们,并传入已膨胀的过渡资源的资源 ID,如下例所示

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 及更高版本中受支持,并且包含旧版框架过渡中不存在的错误修复。

使用共享元素过渡

作为过渡框架的一部分,共享元素过渡确定在片段过渡期间相应视图如何在两个片段之间移动。例如,您可能希望在片段 A 的ImageView中显示的图像在 B 变得可见时过渡到片段 B,如图 3 所示。

A fragment transition with a shared element.
图 3. 带有共享元素的片段过渡。

在高级别上,以下是使用共享元素进行片段过渡的方法

  1. 为每个共享元素视图分配一个唯一的过渡名称。
  2. 将共享元素视图和过渡名称添加到FragmentTransaction
  3. 设置共享元素过渡动画。

首先,您必须为每个共享元素视图分配一个唯一的过渡名称,以允许将这些视图从一个片段映射到下一个片段。使用ViewCompat.setTransitionName()在每个片段布局中的共享元素上设置过渡名称,该方法为 API 级别 14 及更高版本提供兼容性。例如,片段 A 和 B 中ImageView的过渡名称可以按如下方式分配

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.addSharedElement()将每个共享元素添加到您的FragmentTransaction中,传入视图和下一个片段中相应视图的过渡名称,如下例所示

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

要指定共享元素如何从一个片段过渡到下一个片段,您必须在要导航到的片段上设置进入过渡。在片段的onCreate()方法中调用Fragment.setSharedElementEnterTransition(),如下例所示

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 过渡库。
  • 不支持Animation类和框架Transition库。
  • 预测性片段动画仅在运行 Android 14 或更高版本的设备上有效。
  • setCustomAnimationssetEnterTransitionsetExitTransitionsetReenterTransitionsetReturnTransitionsetSharedElementEnterTransitionsetSharedElementReturnTransition都支持预测性后退。

要了解更多信息,请参阅添加对预测性后退动画的支持

推迟过渡

在某些情况下,您可能需要将片段过渡推迟一小段时间。例如,您可能需要等到进入片段中的所有视图都已测量和布局,以便 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();

要推迟进入过渡,请在进入片段的onViewCreated()方法中调用Fragment.postponeEnterTransition()

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

在处理用户网络连接缓慢等情况时,您可能需要在经过一段时间后启动推迟的过渡,而不是等待所有数据加载完毕。对于这些情况,您可以改为在进入片段的onViewCreated()方法中调用Fragment.postponeEnterTransition(long, TimeUnit),传入持续时间和时间单位。然后,一旦指定时间过去,推迟的过渡就会自动开始。

将共享元素过渡与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一起使用时,另一个需要考虑的点是,您不能在RecyclerView项的 XML 布局中设置过渡名称,因为任意数量的项共享该布局。必须分配唯一的过渡名称,以便过渡动画使用正确的视图。

您可以通过在绑定ViewHolder时分配唯一的过渡名称来为每个项目的共享元素提供唯一的过渡名称。例如,如果每个项目的日期包含唯一的 ID,则可以使用它作为过渡名称,如下例所示

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

其他资源

要了解有关片段过渡的更多信息,请参阅以下其他资源。

示例

博文