Fragment API 提供了两种方法来使用运动效果和转换在导航期间视觉连接片段。其中之一是动画框架,它同时使用 Animation
和 Animator
。另一种是 转换框架,其中包括共享元素转换。
您可以为进入和退出片段以及片段之间共享元素的转换指定自定义效果。
- 进入效果确定片段如何进入屏幕。例如,您可以在导航到片段时创建一种效果,使片段从屏幕边缘滑入。
- 退出效果确定片段如何退出屏幕。例如,您可以在导航离开片段时创建一种效果,使片段淡出。
- 共享元素转换确定在两个片段之间共享的视图如何在它们之间移动。例如,在片段 A 的
ImageView
中显示的图像在 B 可见后会过渡到片段 B。
设置动画
首先,您需要为进入和退出效果创建动画,这些动画在导航到新片段时运行。您可以将动画定义为 补间动画资源。这些资源允许您定义片段在动画期间应如何旋转、拉伸、淡入淡出和移动。例如,您可能希望当前片段淡出,而新片段从屏幕右侧滑入,如图 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
动画。例如,当用户返回到上一个屏幕时,您可能希望当前片段从屏幕右侧滑出,而上一个片段淡入。
这些动画可以按如下方式定义
<!-- 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 所示。
在高级别上,以下是使用共享元素进行片段过渡的方法
- 为每个共享元素视图分配一个唯一的过渡名称。
- 将共享元素视图和过渡名称添加到
FragmentTransaction
。 - 设置共享元素过渡动画。
首先,您必须为每个共享元素视图分配一个唯一的过渡名称,以允许将这些视图从一个片段映射到下一个片段。使用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 或更高版本的设备上有效。
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();
要推迟进入过渡,请在进入片段的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); ... } }
其他资源
要了解有关片段过渡的更多信息,请参阅以下其他资源。