过渡动画框架

当用户通过键盘输入或者触发其他事件时界面需要做出变化,比如,某个Activity包含一个搜索框,当用户输入数据并提交的时候,Activity会隐藏搜索框同时显示搜索的结果。

在这种应用场景(Scenes)下,可以通过在不同的View树上运行动画,来提供连续的视觉效果。这些动画不仅仅响应了用户的操作,也帮助用户学习应用是如何工作的。

Android的过渡动画框架(Transitions Framework)可以方便的实现两个View树之间的动画。它通过动态的改变View的属性从而达到动画变换的效果。Android系统内置了常用的动画效果,也可以自定义过渡动画和过渡生命周期。

在Android4.4之前可以使用布局动画实现简单过渡动画效果,但是对于复杂的场景,布局动画使用起来不太方便,所以,Android4.4引入了过渡动画框架来方便实现这样的应用场景。

过渡动画的本质还是属性动画,只不过是对属性动画做了一层封装,目的是方便开发者实现ViewGroup的过渡动画效果。

场景和过渡动画

过渡动画框架可以在两个不同的View树之间运行过渡动画,它对View树中的所有View执行一个或者多个动画,框架的有如下特性:

  • GroupView级别的动画:对View树中的所有View执行动画
  • 基于变化的动画:动画的运行是基于View属性的开始值和结束值
  • 内置动画:包含了一些预置的动画,比如淡入淡出和移动
  • 支持资源文件:可以通过布局文件中加载View树和内置动画
  • 声明周期回调:通过回调可以控制动画的执行过程

概述

过渡动画框架和动画的关系如下所示:

过渡动画框架原理

Transitions框架和View动画是平行关系。Transitions框架主要用于存储View树的状态,在切换View树时执行定义的动画,从而实现过渡效果。

Transitions框架提供了抽象的Scenes、Transition以及TransitionManagers。使用过程中,首先,为执行动画的View树创建初始Scene;然后,为动画创建Transition;最后,使用TransitionManager对象启动动画,并传入创建的Transition和结束Scene。

场景(Scenes)

Scene存储了View树的状态,即View树中所有View的属性值,这样就可以通过改变属性值,从当前Scene变换到指定的Scene。

创建Scene可以通过Layout文件或者在代码中使用GroupView对象。通过代码创建Scene,用于动态生成View树,或者在运行时改变View树。

通常情况下,并不需要创建开始Scene。当使用Transition时,系统会使用上一个Transition的结束Scene作为下一个Transition的开始Scene;如果之前没有使用过Transition,系统会收集View树的当前状态作为开始Scene。

可以定义Scene变化的Action。比如,在Scene变化后,清除View的设置。

Scene除了存储View树的属性值,同时也存储了View树的父视图引用,这个引用称为Scene Root,Scene的变换和动画都发生在Scene Root中。

过渡动画(Transition)

在Transitions框架中,创建了一系列的帧去描述View树从开始Scene到结束Scene的变化。动画的信息都存储在Transition对象中,可以使用TransitionManager对象为动画指定一个Transition。Transition可以用于两个不同的Scene,或者同一个Scene的不同状态。

系统内置了一组常用的Transition,比如淡入淡出,缩放等。可以根据框架中提供的API自定义一个Transition来创建想要动画的效果。也可以组合自定义或者内置Transition,作为一个Transition集合,达到不同的动画效果。

系统会监听Transition整个生命周期,这一点和Activity相似。每个重要的生命周期状态,都会执行一个回调函数,在函数中你可以根据不同的状态调整用户界面。

限制(Limitation)

  • 应用于SurfaceView的动画显示会有问题。SurfaceView的更新通过非UI线程,它的更新与动画中的其他View可能会不同步。
  • 用于TextureView的某些特定Transition类型会产生一些不同与期望的动画效果。
  • 继承于AdapterView的类,例如ListView,管理子View的方式与Transition框架不兼容。如果将动画应用于这些View,会出问题。
  • TextView执行缩放动画时,在动画完成前,文本会被放置到新得到位置。为了避免此问题,不要在包含文本的TextView中使用缩放的动画。

创建场景

Transitions框架可以在开始和结束的Scene中执行过渡动画。开始Scene通常由用户界面的当前状态决定。结束Scene,可以通过资源文件或者使用代码创建。

注意:单个View树的变换可以不使用Scene,具体使用在下一节介绍。

使用XML文件创建Scene

通过资源文件可以直接创建Scene对象。当View树不会变化时可以使用这种方式。生成的Scene代表当你创建Scene实例时View树的状态。如果改变了View树,必须重新创建Scene。创建的Scene包含整个资源文件,不允许从部分资源文件中创建Scene。

从布局文件中创建Scene,首先要从布局文件中获取ViewGroup类型Scene Root对象,接着调用Scene.getSceneForLayout(),参数为Scene Root和布局文件中作为Scene的View树的资源ID。

为Scene定义Layout

下面介绍通过相同的Scene Root元素创建两个不同的Scene。通过代码同样可以看到,只需要加载两个不相关的Scene对象,并不需要定义他们的依赖关系。

示例中的布局为:

  • Activity的主布局文件包含一个TextView和一个子布局
  • 一个相对布局中包含了两个TextView作为第一个Scene
  • 一个相对布局同样包含两个TextView但是顺序不同作为第二个Scene

示例中所有的动画发生在Activity主布局文件的子布局中,而主布局文件中的TextView是不变的。

Activity主布局文件为:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 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>

该布局文件中定义了一个TextView和一个作为Scene Root的子布局。第一个Scene被包含在主布局文件中。App把它作为应用的初始界面,Scene也会把整个子布局加载起来,因为Transition框架是不能够加载部分布局到Scene中。

Scene 1的布局定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- res/layout/a_scene.xml -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scene_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/text_view1
android:text="Text Line 1" />
<TextView
android:id="@+id/text_view2
android:text="Text Line 2" />
</RelativeLayout>

Scene 2包含相同的两个TextView(一样的资源ID),但是他们的顺序和Scene 1不一样:

1
2
3
4
5
6
7
8
9
10
11
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scene_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/text_view2
android:text="Text Line 2" />
<TextView
android:id="@+id/text_view1
android:text="Text Line 1" />
</RelativeLayout>

从Layout文件中生成Scene

1
2
3
4
5
6
7
8
9
10
Scene mAScene;
Scene mAnotherScene;

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

// Create the scenes
mAScene = Scene.getSceneForLayout(mSceneRoot, R.layout.a_scene, this);
mAnotherScene =
Scene.getSceneForLayout(mSceneRoot, R.layout.another_scene, this);

这样,应用就包含了两个基于View树的Scene对象。两个Scene使用同样Scene Root,它通过activity_main.xml中的FrameLayout元素定义。

使用Java代码创建Scene

在代码中通过ViewGroup对象同样可以创建Scene实例。当你在代码中需要修改View树的结构或者动态的生成View树时,可以使用这种方式。

在代码中利用View树创建Scene,使用Scene(sceneRoot, viewHierarchy)构造函数。这个构造函数和Scene.getSceneForLayout()有同样的效果。

1
2
3
4
5
6
7
8
9
10
11
Scene mScene;

// Obtain the scene root element
mSceneRoot = (ViewGroup) mSomeLayoutElement;

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

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

创建Scene Actions

当Scene进入和退出时,系统允许自定义Scene Action。通常情况下自定义Action是没有必要的,因为系统会自动处理Scene间的变换。

Scene Action用于下面几个情况:

  • 执行动画的View不在Scene的View树里,其执行动画的时机可能在Scene开始或者结束时。
  • Transitions框架不支持的View,比如ListView。

可以将Scene Action定义成Runnable对象,然后作为参数调用Scene.setExitAction()和Scene.setEnterAction()方法。在Transition动画运行之前,开始Scene会调用setExitAction()方法,在Transition动画运行之后,结束Scene会调用setEnterAction()方法。

注意:不要在开始Scene和结束Scene中的View之间使用Scene Action传递数据。因为结束View树直到动画结束才会被初始化。

使用过渡动画

在Transitions框架中,动画创建了一系列的帧描述View树从开始Scene到结束Scene之间的变化。这些变化在Transition框架中使用Transition对象来表示,所有动画的信息都包含在其中。通过TransitionManager对象启动动画,具体使用时,需要传入Transition对象和结束Scene。

创建Transition

内置Transition对象的创建方式有两种,可以资源文件定义,也可以直接用代码创建。

系统提供的内置Transition类型如下所示:

Class Tag Effect
AutoTransition <autoTransition/> Default transition. Fade out, move and resize, and fade in views, in that order.
Fade <fade/> fade_in fades in views;
fade_out fades out views;
fade_in_out (default) does a fade_out followed by a fade_in.
ChangeBounds <changeBounds/> Moves and resizes views.

通过资源文件中创建Transition

在资源文件中创建Transition的好处是当你需要修改Transition的定义的时候不用修改Activity中的代码,另一个好处就是将复杂的Transition定义和代码分离。

  • 创建Transition
1
2
<!-- res/transition/fade_transition.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android" />
  • 从资源文件中获取Transition对象
1
2
3
Transition mFadeTransition =
TransitionInflater.from(this).
inflateTransition(R.transition.fade_transition);

通过代码中创建Transition

这种方式的好处是可以动态的创建Transition对象(如果你在代码中需要修改用户界面),而且创建内置Transition需要很少的参数。

1
Transition mFadeTransition = new Fade();

使用Transition

Transition通常用于切换不同的View树,来响应用户操作等事件。例如:当用户输入搜索关键字并点击搜索按钮,应用切换到搜索结果布局,此时搜索界面执行淡出效果,搜索结果界面执行淡入效果。

在Activity中利用Transition切换Scene,通过调用静态方法TransitionManager.go(),并传入结束的Scene和代表动画效果的Transition实例,如下所示:

1
TransitionManager.go(mEndingScene, mFadeTransition);

当运行Transition实例指定的动画时,系统会将Scene Root中的View树切换成结束Scene中的View树。上一个Transition结束的Scene作为开始的Scene,如果不存在上一个Transition,用户界面的当前状态就是开始Scene。

如果没有指定Transition实例,TransitionManager会使用AutoTransition对象,它会执行大多数情况下合理的动画效果。

选择特定的Target View

默认情况下系统对开始和结束Scene中所有的View执行动画。有些时候,只希望仅仅让Scene中的一个子View运行动画。例如,系统不支持ListView对象的动画,在Transition的过程中就必须排除ListView对象。Transition框架允许只让某个特定的View运行动画。

被选定运行动画的View叫Target。你只能选择Scene中View树的子View作为Target。

Target是被保存在列表中,从Target list中删除一个或者多个Target,调用removeTarget()方法,该方法必须在执行动画之前调用。调用addTarget()方法添加View到Target list中。

定义Transition集合

在Transition系统中可以绑定一系列内置或者自定义的动画到Transition集合中。

1
2
3
4
5
6
<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>

通过在Activity中调用TransitionInflater.from()方法在代码中获取XML声明的TransitionSet对象。TransitionSet继承自Transition类,所以TransitionManager可以和使用Transition对象一样使用TransitionSet。

不使用Scene的情况下使用Transition

改变用户界面不仅仅只有通过切换View树这一种方式,还可以在当前View树中通过添加,修改,删除子View修改界面。

如果供选择的两个View树有相似的结构,则可以选择使用这种方式。不必为了用户界面的微小差别而创建和维护两个不同的资源文件,可以在资源文件中定义View树然后在代码中修改它的结构。

如果只是在一个View树改动,则不必创建Scene。而是使用delayed transition的方式在一个View树的两个状态间创建和使用Transition。Transition框架记录View树的当前状态和View的变动,在系统重绘用户界面时对View的变化执行Transition动画。

在单一的View树中创建delayed transition,遵循以下步骤:

  • 当事件触发了Transition,调用TransitionManager.beginDelayedTransition()方法,传入待执行动画View的父View和Transition。系统会保存View的当前状态和属性值。
  • 根据Transition改变子View。系统会记录哪些子View的哪些属性被改变了。
  • 当系统根据你的变化重绘用户界面时,会在初始状态和最终状态间执行动画。

下面的代码演示了如何使用delayed transition添加一个TextView到一个View树中:

  • View树对应的资源文件
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- res/layout/activity_main.xml -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
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" />
...
</RelativeLayout>
  • 添加TextView时执行动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* MainActivity.java */

private TextView mLabelText;
private Fade mFade;
private ViewGroup mRootView;
...

// Load the layout
this.setContentView(R.layout.activity_main);
...

// Create a new TextView and set some View properties
mLabelText = new TextView();
mLabelText.setText("Label").setId("1");

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

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

// Add the new TextView to the view hierarchy
mRootView.addView(mLabelText);

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

定义Transition的生命周期回调

Transition的生命周期和Activity相似。它代表从调用TransitionManager.go()方法到动画运行结束过程中的状态。重要的生命状态,系统会执行TransitionListener中的回调方法。

Transition的生命周期回调是非常有用的,比如,在Scene变化过程中将某个View的属性值从开始View树中复制到结束View树中。由于结束View树直到动画结束才会被初始化,所以简单的复制值会出问题。这种情况下,可以先将值存储在一个变量中,然后当动画结束后再复制它到结束View树中。获取动画结束事件可以在Activity中实现TransitionListener.onTransitionEnd()回调方法。

参考资料

Animating Views Using Scenes and Transitions

坚持原创技术分享,您的支持将鼓励我继续创作!