当用户通过键盘输入或者触发其他事件时界面需要做出变化。比如,某个Activity包含一个搜索框,当用户输入数据并提交的时候,Activity会隐藏搜索框同时显示搜索的结果。
在这种应用场景下你可以通过在不同的View树运行动画,来提供连续的视觉效果。动画不仅仅响应了用户的操作也帮助用户学习应用是如何工作的。
Android的Transitions 框架让开发者可以方便的实现两个View树之间的动画。Transitions 框架是通过动态的改变View的属性从而达到动画变换的效果。Android系统内置了常用的动画效果,开发者也可以自定义动画和变换(的生命周期)。
本文介绍如何使用transitions 框架内置的动画,当然你也可以创建自定义动画(和动画生命周期的回调)。
- 注意:Android版本在14(4.0)-19(4.4.2)之间的用户,使用animateLayoutChanges属性设置布局动画。了解更多请查看属性动画。
一.Transitions框架--The Transitions Framework
在你的应用中加入动画不仅仅是增加了视觉的吸引力,动画所强调的变化和视觉线索可以帮助用户学习如何使用你的应用。
Android系统提供了Transitions 框架,使得你可以在两个不同的View树之间运行动画。当动画在两个不同的View树间运行时,Transitions 框架将对View树中所有的View执行一个或者多个动画。
框架的特性:
- Group级的动画:对View树中的所有View执行一个或多个动画
- 基于变化的动画:动画的运行是基于View属性的开始值和结束值
- 内置动画:包含了一些预置的动画,比如淡入淡出和移动
- 支持资源文件:可以在布局文件中加载内置的动画
- 生命周期回调:定义了回调函数,在动画的执行过程中进行控制
1.概述
Transition框架中执行动画会改变两个View树中的所有View。View树可以是一个简单的View也可能是一个类似ViewGroup包含一个树状结构的视图。从动画的初始状态(View开始树)到终止状态(View结束树),每个View的一个或多个属性在动画的运行过程中都会发生变化。
Transition框架与View树和动画是平行的关系。Transition框架主要作用是存储View树的状态,切换View树以改变屏幕的显示效果,并在切换View树的时候执行定义的动画。
图二演示了View树,Transition框架和动画之间的关系:
Transition框架提供了抽象的Scenes,Transition,Transition managers。使用Transition框架,首先为即将运行动画的View树创建Scenes。然后,为动画创建Transition。启动两个View树间的动画,使用Transition Manager对象,传入指定的Transition和结束Scene。
2.场景 -- Scenes:
Scene存储了View树的状态,包括View树中的所有View和这些View的属性。View树可以是简单的View也可能是复杂的ViewGroup或者layout。Scene中存储了View树的状态,使得你可以从另一个Scene变换到当前的Scene。
创建Scene可以通过资源文件或者在代码中使用ViewGroup对象。通过代码创建Scene的好处就是View树可以动态的生成并且在运行过程也可以被修改。
通常情况下,你并不需要创建开始的场景。当你使用一个Transition时,系统会使用上一个Transition的结束Scene作为下一个Transition的开始Scene。如果你没有使用Transition,系统则会收集View的当前状态作为开始的场景。
Scene变换时,也可以定义它的运行动作。例如,这一特性用于在Scene变换后清除View的设置。
Scene除了存储了View树和View树的属性值,同时也存储了View树的父视图的引用。这个父视图叫Scene Root。Scene的变换和动画都发生在Scene Root中。
3.Transition
在Transition框架中,动画创建了一系列的帧,描述了View树从开始Scene到结束Scene的变化。动画的信息都存储在Transition对象中。可以使用TransitionManager对象为动画指定一个Transition。Transition可以用于两个不同的Scene,或者是同一Scene的不同状态(译者注:View属性的不同)。
系统内置了一组常用动画的Transition,比如淡入淡出,缩放等。你可以根据框架中提供的API自定义一个Transition来创建想要动画的效果。同样你也可以组合不同的动画效果到一个包含自定义或者内置Transition的Transition集合中。
系统监控Transition生命周期,从动画的开始到动画的结束,这一点和Activity相似。每个重要的生命周期状态,都会执行一个回调函数,在函数中你可以根据不同的状态调整用户界面。
4.限制 -- Limitations
Transition框架的限制:
- 应用于SurfaceView的动画显示会有问题。SurfaceView的更新通过非UI线程,所以它的更新与动画中的其他View可能会不同步。
- 应用于TextureView的某些特定Transition类型会产生一些不同与期望的动画效果。
- 继承自AdapterView的类,例如ListView,管理子View的方式与Transition框架不兼容。如果将动画应用于这些View,会出问题。
- TextView执行缩放动画时,在动画完成前,文本会被放置到新得到位置。为了避免此问题,不要在包含文本的TextView中使用缩放的动画。
二.创建场景--Creating a Scene
Scene存储了View树的状态,包括View树中的所有View和属性值。Transition框架可以在开始和结束的Scene中执行动画。开始Scene通常由用户界面的当前状态决定。结束Scene,可以通过资源文件或者使用代码创建。
本文介绍如何在你的应用中创建Scene并定义Scene Action。下一节介绍如何在两个Scene中切换。
注意:单个View树的变换可以不使用Scene。
1.通过资源文件创建场景 -- Create a Scene From a Layout Resource
通过资源文件可以直接创建Scene对象。当View树不会变化时可以使用这种方式。生成的Scene代表当你创建Scene实例时View树的状态。如果改变了View树,必须重新创建Scene。创建的Scene包含整个资源文件,不允许从部分资源文件中创建Scene。
想要从布局文件中创建Scene,首先从布局文件中获取ViewGroup类型Scene Root对象,接着调用Scene.getSceneForLayout(),参数为Scene Root和布局文件中作为Scene的View树的资源ID。
为场景定义资源文件 -- Define Layouts for Scenes
接下来演示如何通过相同的Scene Root元素创建两个不同的Scene。通过代码同样可以看到你只需要加载两个不相关的Scene对象并不需要定义他们的依赖关系。
示例中的布局为:
- Activity的主布局文件包含一个TextView和一个子布局
- 一个相对布局中包含了两个TextView作为第一个Scene
- 一个相对布局同样包含两个TextView但是顺序不同作为第二个Scene
示例中所有的动画发生在Activity主布局文件的子布局中,而主布局文件中的TextView是不变的。
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>
该布局文件中定义了一个TextView和一个作为Scene Root的子布局。第一个Scene被包含在主布局文件中。App把它作为应用的初始界面,Scene也会把整个子布局加载起来,因为Transition系统是不能够加载部分布局到Scene中。
Scene 1的布局定义如下:
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不一样:
res/layout/anther_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_view2
android:text="Text Line 2" />
<TextView
android:id="@+id/text_view1
android:text="Text Line 1" />
</RelativeLayout>
从Layout文件中生成Scene -- Generate Scenes from Layouts
定义好两个布局文件后,就可以获取Scene对象了。这样你就能够在这两个UI配置间执行Transition动画。获取Scene对象通过Scene Root和布局的资源ID。
下面的代码演示了如何获取Scene Root的和通过布局文件创建Scene:
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元素定义。
2.通过代码创建Scene
在代码中通过ViewGroup对象同样可以创建Scene实例。当你在代码中需要修改View树的结构或者动态的生成View树时,可以使用这种方式。
在代码中利用View树创建Scene,使用Scene(sceneRoot,viewHierarchy)构造函数。这个构造函数和Scene.getSceneForLayout()有同样的效果。
下面的代码演示了如何根据Scene Root元素和View树中创建Scene:
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);
3.创建Scene动作 -- Create Scene Actions
当Scene进入和退出时,系统允许你自定义Scene Action。通常情况下自定义Action是没有必要的,因为系统会自动处理Scene间的变换。
Scene Action用于下面几个情况:
- 执行动画的View不在同一个View树里,View执行动画的时机可能是在Scene开始或者结束时。
- Transition框架不支持的View,比如ListView。
将自定义的Scene Action定义成Runnable对象,然后作为参数调用Scene.setExitAction()和Scene.setEnterAction()方法。在开始Scene运行Transition动画之前,系统会调用setExitAction()方法,结束Scene运行Transition动画之后会调用setEnterAction()方法。
注意:不要在结束Scene和开始Scene的View间使用Scene Action传递数据。因为结束View树直到动画结束才会被初始化。
三.使用Transition -- Applying a Transition
在Transition框架中,动画创建了一系列的帧描述View树从开始Scene到结束Scene之间的变化。这些变化在Transition框架中使用Transition对象来表示,所有动画的信息都包含在其中。启动动画使用Transition Manager,传入使用的Transition对象和结束的Scene。
接下来介绍在两个Scene间使用内置的Transition运行移动,缩放,淡入淡出等动画。
1.创建Transition -- Create a Transition
在上一节,介绍了如何创建代表不同View树状态的Scene对象。有了开始的Scene和结束的Scene之后就需要创建定义动画的Transition对象。内置Transition对象的创建方式有两种,可以资源文件定义,也可以直接用代码创建。
内置的Transition类型
Class | Tag | Attributes | Effect |
---|---|---|---|
AutoTransition | <autoTransition/> | - | Default transition. Fade out, move and resize, and fade in views, in that order. |
Fade | <fade/> | android:fadingMode="[fade_in | | fade_in fades in viewsfade_out fades out viewsfade_in_out (default) does a fade_out followed by a fade_in . |
ChangeBounds | <changeBounds/> | - | Moves and resizes views. |
在资源文件中创建Transition -- Create a Transition instance from a resource file
在资源文件中创建Transition的好处是当你需要修改Transition的定义的时候不用修改Activity中的代码。另一个好处就是将复杂的Transition定义和代码分离。
在资源文件中创建内置的Transition,遵循以下几步:
- 添加res/transition/目录到工程里
- 在该目录下创建XML文件
- 添加内置的Transition作为一个XML节点
例如,下面的资源文件声明了一个Fade Transition:
res/transition/fade_transition.xml
<fade xmlns:android="http://schemas.android.com/apk/res/android" />
接下来从资源文件中获取Transition对象
Transition mFadeTransition =
TransitionInflater.from(this).
inflateTransition(R.transition.fade_transition);
在代码中创建Transition -- Create a Transition instance in your code
这种方式的好处是可以动态的创建Transition对象(如果你在代码中需要修改用户界面),而且创建内置Transition需要很少的参数。
创建内置的Transition对象,通过调用Transition子类的public构造函数。如下所示:
Transition mFadeTransition = new Fade();
2.使用Transition -- Apply a Transition
Transition通常用于切换不同的View树,来响应用户操作等事件。例如:当用户输入搜索关键字并点击搜索按钮,应用切换到搜索结果布局,此时搜索界面执行淡出效果,搜索结果界面执行淡入效果。
在Activity中利用Transition切换Scene,通过调用静态方法TransitionManager.go(),并传入结束的Scene和代表动画效果的Transition实例,如下所示:
TransitionManager.go(mEndingScene, mFadeTransition);
当运行Transition实例指定的动画时,系统会将Scene Root中的View树切换成结束Scene中的View树。上一个Transition结束的Scene作为开始的Scene,如果不存在上一个Transition,用户界面的当前状态就是开始Scene。
如果没有指定Transition实例,TransitionManager会使用AutoTransition对象,它会执行大多数情况下合理的动画效果。更多的信息请参照TransitionManager类。
3.选择特定的Target View -- Choose Specific Target Views
默认情况下系统对开始和结束Scene中所有的View执行动画。有些时候,你或许希望仅仅让Scene中的一个子View运行动画。例如,系统不支持ListView对象的动画,在Transition的过程中就必须剔除ListView对象。系统允许你选定某个特定的View运行动画。
被选定运行动画的View叫Target。你只能选择Scene中View树的子View作为Target。
Target是被保存在列表中,从Target list中删除一个或者多个Target,调用removeTarget()方法,该方法必须在执行动画之前调用。调用addTarget()方法添加View到Target list中。更多信息请参照Transition类。
4.定义复杂的Transition -- Specify Multiple Transitions
动画的定义应该匹配Scene之间的变化。比如,Scene之间的变化是,删除一个View并添加另一个View,那么淡入/淡出动画可以更好的提示用户有些View不可见了。如果仅仅将View移动到另一个位置,那么使用移动动画告诉用户View移动到了新的位置则是更好的选择。
在Transition系统中可以绑定一系列内置或者自定义的动画到Transition集合中。
在XML中定义Transition集合,首先在/res/transitions/目录中创建资源文件,然后在transitionSet元素下定义Transition。下面的代码定义了一个和AutoTransition类相同行为的transition集合:
<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>
在代码中获取XML声明的TransitionSet对象,通过在Activity中调用TransitionInflater.from()方法。TransitionSet继承自Transition类,所以TransitionManager可以和使用Transition对象一样使用TransitionSet。
5.不使用Scene的情况下使用Transition -- Apply a Transition Without Scene
改变用户界面不仅仅只有通过切换View树这一种方式。你同样可以通过添加,修改,删除子View的方式。例如,你可以在一个布局文件中实现搜索的交互。首先在布局中显示搜索输入框和搜索按钮,接着当用户点击搜索按钮时,显示搜索结果同时通过ViewGroup.removeView()方法删除搜索按钮,添加搜索结果则通过ViewGroup.addView()方法。
如果供选择的两个View树有着相似的结构,则可以选择使用这种方式。不必为了用户界面的微小差别而创建和维护两个不同的资源文件,可以在资源文件中定义View树然后在代码中修改它的结构。
如果只是在一个View树改动,则不必创建Scene。而是使用delayed transition的方式在一个View树的两个状态间创建和使用Transition 。Transition框架记录View树的当前状态和View的变动,在系统重绘用户界面时对View的变化执行Transition动画。
在单一的View树中创建delayed transition,遵循以下步骤:
- 当事件触发了Transition,调用TransitionManager.beginDelayedTransition()方法,传入待执行动画View的父View和Transition。系统会保存View的当前状态和属性值。
- 根据你的需要改变子View。系统会记录哪些子View的哪些属性被改变了。
- 当系统根据你的变化重绘用户界面时,会在初始状态和最终状态间执行动画。
下面的代码演示了如何使用delayed transition添加一个TextView到一个View树中,首先资源文件如下所示:
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上执行动画:
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
6.定义Transition的生命周期回调 -- Define Transition Lifecycle Callbacks
Transition的生命周期和Activity相似。它代表从调用TransitionManager.go()方法到动画运行结束过程中的状态。重要的生命状态,系统会执行TransitionListener中的回调方法。
Transition的生命周期回调是非常有用的,比如,在Scene变化过程中将某个View的属性值从开始View树中复制到结束View树中。由于结束View树直到动画结束才会被初始化,所以简单的复制值会出问题。这种情况下,你可以先将值存储在一个变量中,然后当动画结束后再复制它到结束View树中。那么如何获取动画结束的通知,在Activity中实现TransitionListener.onTransitionEnd()即可。
更多信息,请参考TransitionListener类的API。