使用过渡为布局变化添加动画

试试 Compose 的方式
Jetpack Compose 是 Android 推荐的 UI 工具包。了解如何在 Compose 中使用动画。

Android 的过渡框架允许您通过提供开始和结束布局,为界面中的各种动作添加动画。您可以选择想要的动画类型,例如视图的淡入淡出或改变视图大小,过渡框架会确定如何从开始布局动画到结束布局。

过渡框架包含以下功能:

  • 组级别动画:将动画效果应用于视图层次结构中的所有视图。
  • 内置动画:使用预定义的动画实现常见效果,例如淡出或移动。
  • 资源文件支持:从布局资源文件加载视图层次结构和内置动画。
  • 生命周期回调:接收回调,用于控制动画和层次结构变化过程。

有关布局变化动画的示例代码,请参阅BasicTransition

在两个布局之间添加动画的基本过程如下:

  1. 为开始和结束布局创建Scene对象。但是,开始布局的场景通常从当前布局自动确定。
  2. 创建Transition对象来定义您想要的动画类型。
  3. 调用TransitionManager.go(),系统会运行动画来交换布局。

图 1 中的图示说明了布局、场景、过渡和最终动画之间的关系。

图 1. 过渡框架如何创建动画的基本图示。

创建场景

场景存储视图层次结构的状态,包括其所有视图及其属性值。过渡框架可以在开始场景和结束场景之间运行动画。

您可以从布局资源文件或代码中的视图组创建场景。但是,过渡的开始场景通常会从当前界面自动确定。

场景还可以定义自己的操作,这些操作在您更改场景时运行。此功能对于在过渡到场景后清理视图设置非常有用。

从布局资源创建场景

您可以直接从布局资源文件创建Scene实例。当文件中的视图层次结构大部分是静态的时,请使用此技术。生成的场景表示您创建Scene实例时视图层次结构的状态。如果更改视图层次结构,请重新创建场景。框架从文件中的整个视图层次结构创建场景。您不能从布局文件的一部分创建场景。

要从布局资源文件创建Scene实例,请将布局中的场景根目录检索为ViewGroup。然后,使用场景根目录和包含场景视图层次结构的布局文件的资源 ID 调用Scene.getSceneForLayout()函数。

为场景定义布局

本节其余部分的以下代码片段展示了如何使用相同的场景根元素创建两个不同的场景。这些片段还演示了您可以加载多个不相关的Scene对象,而无需暗示它们之间存在关联。

此示例包含以下布局定义:

  • activity 的主布局,带有一个文本标签和一个子FrameLayout
  • 第一个场景的ConstraintLayout,包含两个文本字段。
  • 第二个场景的ConstraintLayout,包含相同两个文本字段,顺序不同。

该示例的设计旨在让所有动画都发生在 activity 主布局的子布局内。主布局中的文本标签保持静态。

activity 的主布局定义如下:

res/layout/activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/master_layout">
    <TextView
        android:id="@+id/title"
        ...
        android:text="Title"/>
    <FrameLayout
        android:id="@+id/scene_root">
        <include layout="@layout/a_scene" />
    </FrameLayout>
</LinearLayout>

此布局定义包含一个文本字段和一个用于场景根的子FrameLayout。第一个场景的布局包含在主布局文件中。这使得应用可以将其显示为初始用户界面的一部分,并且也可以将其加载到场景中,因为框架只能将整个布局文件加载到场景中。

第一个场景的布局定义如下:

res/layout/a_scene.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:id="@+id/text_view1"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 1"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
    <TextView
        android:id="@+id/text_view2"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 2"
        app:layout_constraintTop_toBottomOf="@id/text_view1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

第二个场景的布局包含相同的两个文本字段,具有相同的 ID,顺序不同。定义如下:

res/layout/another_scene.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:id="@+id/text_view2"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 2"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    <TextView
        android:id="@+id/text_view1"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 1"
        app:layout_constraintTop_toBottomOf="@id/text_view2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

从布局生成场景

创建两个 ConstraintLayout 的定义后,可以获取它们各自的场景。这使您可以在两种界面配置之间进行过渡。要获取场景,您需要场景根和布局资源 ID 的引用。

以下代码片段展示了如何获取场景根的引用并从布局文件创建两个Scene对象:

Kotlin

val sceneRoot: ViewGroup = findViewById(R.id.scene_root)
val aScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this)
val anotherScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this)

Java

Scene aScene;
Scene anotherScene;

// Create the scene root for the scenes in this app.
sceneRoot = (ViewGroup) findViewById(R.id.scene_root);

// Create the scenes.
aScene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this);
anotherScene =
    Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this);

在应用中,现在有两个基于视图层次结构的Scene对象。两个场景都使用res/layout/activity_main.xml文件中由FrameLayout元素定义的场景根。

在代码中创建场景

您也可以在代码中从ViewGroup对象创建Scene实例。当您直接在代码中修改视图层次结构或动态生成它们时,请使用此技术。

要在代码中从视图层次结构创建场景,请使用Scene(sceneRoot, viewHierarchy)构造函数。调用此构造函数等效于在您已经膨胀布局文件时调用Scene.getSceneForLayout()函数。

以下代码片段演示了如何在代码中从场景根元素和场景视图层次结构创建Scene实例:

Kotlin

val sceneRoot = someLayoutElement as ViewGroup
val viewHierarchy = someOtherLayoutElement as ViewGroup
val scene: Scene = Scene(sceneRoot, viewHierarchy)

Java

Scene mScene;

// Obtain the scene root element.
sceneRoot = (ViewGroup) someLayoutElement;

// Obtain the view hierarchy to add as a child of
// the scene root when this scene is entered.
viewHierarchy = (ViewGroup) someOtherLayoutElement;

// Create a scene.
mScene = new Scene(sceneRoot, mViewHierarchy);

创建场景操作

框架允许您定义自定义场景操作,系统在进入或退出场景时会运行这些操作。在许多情况下,定义自定义场景操作是不必要的,因为框架会自动对场景之间的变化进行动画处理。

场景操作对于处理以下情况非常有用:

  • 为不在同一层次结构中的视图添加动画。您可以使用退出和进入场景操作为开始和结束场景的视图添加动画。
  • 为过渡框架无法自动动画的视图添加动画,例如ListView对象。有关详细信息,请参阅局限性一节。

提供自定义场景操作,将您的操作定义为Runnable对象,并将它们传递给Scene.setExitAction()Scene.setEnterAction()函数。框架会在运行过渡动画之前调用开始场景的setExitAction()函数,并在运行过渡动画之后调用结束场景的setEnterAction()函数。

应用过渡

过渡框架用Transition对象表示场景之间的动画样式。您可以使用内置子类(如AutoTransitionFade)实例化Transition,或者定义自己的过渡。然后,通过将结束SceneTransition传递给TransitionManager.go(),您可以在场景之间运行动画。

过渡生命周期与 activity 生命周期相似,表示框架在动画开始和完成之间监视的过渡状态。在重要的生命周期状态下,框架会调用您可以实现的毁掉函数,以便在过渡的不同阶段调整您的用户界面。

创建过渡

上一节介绍了如何创建表示不同视图层次结构状态的场景。定义要切换的开始和结束场景后,创建定义动画的Transition对象。框架允许您在资源文件中指定内置过渡并在代码中膨胀它,或者直接在代码中创建内置过渡的实例。

表 1. 内置过渡类型。

标记 效果
AutoTransition <autoTransition/> 默认过渡。按顺序淡出、移动和调整视图大小,然后淡入视图。
ChangeBounds <changeBounds/> 移动和调整视图大小。
ChangeClipBounds <changeClipBounds/> 在场景变化前后捕获View.getClipBounds(),并在过渡期间为这些变化添加动画。
ChangeImageTransform <changeImageTransform/> 在场景变化前后捕获ImageView的矩阵,并在过渡期间为其添加动画。
ChangeScroll <changeScroll/> 在场景变化前后捕获目标的滚动属性,并为任何变化添加动画。
ChangeTransform <changeTransform/> 在场景变化前后捕获视图的缩放和旋转,并在过渡期间为这些变化添加动画。
Explode <explode/> 跟踪开始和结束场景中目标视图可见性的变化,并从场景边缘将视图移入或移出。
Fade <fade/> fade_in 淡入视图。
fade_out 淡出视图。
fade_in_out (默认) 先进行fade_out,然后进行fade_in
Slide <slide/> 跟踪开始和结束场景中目标视图可见性的变化,并从场景边缘将视图移入或移出。

从资源文件创建过渡实例

此技术允许您修改过渡定义,而无需更改 activity 的代码。此技术对于将复杂的过渡定义与应用代码分开也很有用,如指定多个过渡一节所示。

要在资源文件中指定内置过渡,请按照以下步骤操作:

  • 向您的项目添加res/transition/目录。
  • 在此目录中创建新的 XML 资源文件。
  • 为其中一个内置过渡添加 XML 节点。

例如,以下资源文件指定了Fade过渡:

res/transition/fade_transition.xml

<fade xmlns:android="http://schemas.android.com/apk/res/android" />

以下代码片段展示了如何在 activity 中从资源文件膨胀Transition实例:

Kotlin

var fadeTransition: Transition =
    TransitionInflater.from(this)
                      .inflateTransition(R.transition.fade_transition)

Java

Transition fadeTransition =
        TransitionInflater.from(this).
        inflateTransition(R.transition.fade_transition);

在代码中创建过渡实例

如果您在代码中修改用户界面,或者需要创建参数很少或没有参数的简单内置过渡实例,此技术非常有用。

要创建内置过渡的实例,请调用Transition类的子类中的公共构造函数之一。例如,以下代码片段创建了Fade过渡的实例:

Kotlin

var fadeTransition: Transition = Fade()

Java

Transition fadeTransition = new Fade();

应用过渡

您通常会为了响应事件(例如用户操作)而在不同的视图层次结构之间应用过渡。例如,考虑一个搜索应用:当用户输入搜索词并点击搜索按钮时,应用会更改为表示结果布局的场景,同时应用一个过渡,使搜索按钮淡出,搜索结果淡入。

要为了响应 activity 中的事件而更改场景并应用过渡,请调用TransitionManager.go()类函数,并提供结束场景和用于动画的过渡实例,如下面的代码片段所示:

Kotlin

TransitionManager.go(endingScene, fadeTransition)

Java

TransitionManager.go(endingScene, fadeTransition);

框架会使用结束场景中的视图层次结构更改场景根中的视图层次结构,同时运行过渡实例指定的动画。开始场景是上一次过渡的结束场景。如果没有上一次过渡,则开始场景会根据用户界面的当前状态自动确定。

如果您未指定过渡实例,过渡管理器可以应用自动过渡,这在大多数情况下都能合理运行。有关详细信息,请参阅TransitionManager类的 API 参考。

选择特定的目标视图

默认情况下,框架会将过渡应用于开始和结束场景中的所有视图。在某些情况下,您可能只想对场景中的一部分视图应用动画。框架允许您选择要动画的特定视图。例如,框架不支持对ListView对象的变化进行动画处理,因此在过渡期间不要尝试对其进行动画处理。

过渡动画处理的每个视图称为目标。您只能选择属于场景关联的视图层次结构一部分的目标。

要从目标列表移除一个或多个视图,请在启动过渡之前调用removeTarget()方法。要仅将您指定的视图添加到目标列表,请调用addTarget()函数。有关详细信息,请参阅Transition类的 API 参考。

指定多个过渡

为了获得最佳的动画效果,将其与场景之间发生的变化类型相匹配。例如,如果您在场景之间移除了一些视图并添加了其他视图,淡出或淡入动画会提供明显的指示,表明某些视图不再可用。如果您将视图移动到屏幕上的不同位置,最好对移动进行动画处理,以便用户注意到视图的新位置。

您不必只选择一个动画,因为过渡框架允许您将动画效果组合在一个过渡集中,该集合包含一组单独的内置或自定义过渡。

要在 XML 中从过渡集合定义过渡集,请在res/transitions/目录中创建一个资源文件,并在TransitionSet元素下列出过渡。例如,以下代码片段展示了如何指定具有与AutoTransition类相同行为的过渡集:

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="sequential">
    <fade android:fadingMode="fade_out" />
    <changeBounds />
    <fade android:fadingMode="fade_in" />
</transitionSet>

要在代码中将过渡集膨胀到TransitionSet对象中,请在 activity 中调用TransitionInflater.from()函数。TransitionSet类继承自Transition类,因此您可以像使用任何其他Transition实例一样将其与过渡管理器一起使用。

在没有场景的情况下应用过渡

更改视图层次结构并非修改用户界面的唯一方式。您还可以通过在当前层次结构中添加、修改和移除子视图来进行更改。

例如,您可以使用单个布局实现搜索交互。从显示搜索输入字段和搜索图标的布局开始。要将用户界面更改为显示结果,当用户点击搜索按钮时,通过调用ViewGroup.removeView()函数移除搜索按钮,并通过调用ViewGroup.addView()函数添加搜索结果。

如果替代方案是创建两个几乎相同的层次结构,您可以使用此方法。您可以只有一个布局文件,其中包含可在代码中修改的视图层次结构,而不是创建和维护两个单独的布局文件来处理用户界面的微小差异。

如果以这种方式在当前视图层次结构内进行更改,则无需创建场景。相反,您可以使用延迟过渡在视图层次结构的两种状态之间创建和应用过渡。过渡框架的此功能从当前视图层次结构状态开始,记录您对其视图所做的更改,并在系统重新绘制用户界面时应用动画处理这些更改的过渡。

要在单个视图层次结构中创建延迟过渡,请按照以下步骤操作:

  1. 当触发过渡的事件发生时,调用TransitionManager.beginDelayedTransition()函数,提供要更改的所有视图的父视图以及要使用的过渡。框架会存储子视图的当前状态及其属性值。
  2. 根据您的用例需要,更改子视图。框架会记录您对子视图及其属性所做的更改。
  3. 当系统根据您的更改重新绘制用户界面时,框架会动画处理原始状态和新状态之间的变化。

以下示例展示了如何使用延迟过渡动画化将文本视图添加到视图层次结构。第一个代码片段显示了布局定义文件:

res/layout/activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/mainLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <EditText
        android:id="@+id/inputText"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    ...
</androidx.constraintlayout.widget.ConstraintLayout>

下一个代码片段显示了动画化文本视图添加的代码:

MainActivity

Kotlin

setContentView(R.layout.activity_main)
val labelText = TextView(this).apply {
    text = "Label"
    id = R.id.text
}
val rootView: ViewGroup = findViewById(R.id.mainLayout)
val mFade: Fade = Fade(Fade.IN)
TransitionManager.beginDelayedTransition(rootView, mFade)
rootView.addView(labelText)

Java

private TextView labelText;
private Fade mFade;
private ViewGroup rootView;
...
// Load the layout.
setContentView(R.layout.activity_main);
...
// Create a new TextView and set some View properties.
labelText = new TextView(this);
labelText.setText("Label");
labelText.setId(R.id.text);

// Get the root view and create a transition.
rootView = (ViewGroup) findViewById(R.id.mainLayout);
mFade = new Fade(Fade.IN);

// Start recording changes to the view hierarchy.
TransitionManager.beginDelayedTransition(rootView, mFade);

// Add the new TextView to the view hierarchy.
rootView.addView(labelText);

// When the system redraws the screen to show this update,
// the framework animates the addition as a fade in.

定义过渡生命周期回调

过渡生命周期与 activity 生命周期相似。它表示框架在调用TransitionManager.go()函数到动画完成期间监视的过渡状态。在重要的生命周期状态下,框架会调用由TransitionListener接口定义的回调。

过渡生命周期回调非常有用,例如,在场景变化期间将视图属性值从开始视图层次结构复制到结束视图层次结构。您不能简单地将值从其开始视图复制到结束视图层次结构中的视图,因为结束视图层次结构直到过渡完成才被膨胀。相反,您需要将值存储在变量中,然后在框架完成过渡后将其复制到结束视图层次结构中。要了解过渡何时完成,请在 activity 中实现TransitionListener.onTransitionEnd()函数。

有关详细信息,请参阅TransitionListener类的 API 参考。

限制

本节列出了过渡框架的一些已知限制:

  • 应用于SurfaceView的动画可能显示不正确。SurfaceView实例从非 UI 线程更新,因此更新可能与对其他视图的动画不同步。
  • 应用于TextureView时,某些特定的过渡类型可能不会产生所需的动画效果。
  • 继承AdapterView的类(例如ListView)管理其子视图的方式与过渡框架不兼容。如果您尝试对基于AdapterView的视图进行动画处理,设备显示可能会停止响应。
  • 如果您尝试使用动画调整TextView的大小,文本会在对象完全调整大小之前突然移动到新位置。为了避免此问题,请勿对包含文本的视图进行大小调整动画处理。