原文:Bitesize Android KitKat: Week 6: Transitions Framework
注:本文的几个专用术语:
transition 过渡动画
Scene 场景
这两个术语都有相应的类与之对应。
Android KitKat中让人兴奋的新增特性之一就是新的transitions 框架,它以声明的方式轻易就能创建复杂的动画。
在Bitesize Android KitKat系列的最后一部分,我们将看看transitions framework的组成部分,然后学习如何使用自定义的transitions建立一个示例应用。
代码可以在github 获得 github.com/ShinobiControls/bitesize-kitkat -可以随意下载下来尝试。如果你遇到什么问题,也非常乐意看到修复了的pull request 。代码在Android Studio 0.5.1上测试过。< 0.5 的版本不支持transition 的XML 资源文件。
这篇文章时系列文章 -Bitesize Android KitKat 的一部分,该系列文章讨论了KitKat针对开发者的新特性。每篇文章都带有演示如何在实际场景中使用新特性的示例程序,所有的代码都可以在GitHub 得到。可以查看到目前为止发布的所有文章的 目录 。
Transition framework的基本原理就是view当前的状态被捕获在一个Scene对象中,然后用Transition对象决定Scene之间的动画。TransitionManager用于运行transition,同时维护在特定scene之间过渡时该使用什么transition的规则。
注:三个概念:Scene,Transition,TransitionManager。
一个Scene是基于一个ViewGroup构造的,那么我们来创建两个用于scene的布局资源。第一个是一个简单的LinearLayout,里面包含了一张图片。一个标题文字以及一个大点的显示内容的文本。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayout" > <ImageView android:layout_width="300dp" android:layout_height="300dp" android:id="@+id/story_image" android:layout_gravity="center_horizontal" android:src="@drawable/sample1"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceLarge" android:text="Title Text" android:id="@+id/story_title" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/story_content" android:text="Content Text" /> </LinearLayout>
第二个只含有一张图片以及一样用于显示内容的文本。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayout" > <ImageView android:layout_width="fill_parent" android:layout_height="150dp" android:scaleType="centerCrop" android:id="@+id/story_image" android:layout_gravity="center_horizontal" android:src="@drawable/sample1"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/story_content" android:text="Content Text" android:textAlignment="center" /> </LinearLayout>
注意两个布局之间的id是相匹配的。transition framework 自动让两个scene中的view呈现动画,而它们是通过ID识别的。
现在我们可以使用静态的Scene.getSceneForLayout()方法从布局创建Scene对象了。值得一提的是这个方法会缓存scene。这对稍后使用xml定义transition有帮助。
下面的代码创建了一个Scene对象的ArrayList:
// Get hold of some relevant content final ViewGroup container = (ViewGroup)findViewById(R.id.container); //我们要在哪些布局之间产生过渡效果。 List<Integer> sceneLayouts = Arrays.asList(R.layout.content_scene_00, R.layout.content_scene_01); // 创建 scenes sceneList = new ArrayList<Scene>(); for(int layout : sceneLayouts) { // 创建scene Scene scene = Scene.getSceneForLayout(container, layout, this); // 把 scene 保存到 sceneList.add(scene); }
getSceneForLayout()方法占用了3个参数:
sceneRoot是一个ViewGroup,代表了所有transition发生所在的容器。
layoutId是创建Scene所需要的布局的ID。
context是一个Context类的对象,用于持有LayoutInflator,以便inflate 前面所讲的layout。
TransitionManager用于实际执行transition
TransitionManager.go(sceneList.get(tab.getPosition()));
这里用到了go()方法。它只有一个参数,一个scene 对象。我们把scene 从之前做好的ArrayList中取出来。
如果你现在就运行app,然后在不同的tab间切换你会看到tab切换的同时内容会自动播放动画:
你都还没有指定动画该如何过渡的任何东西,而在这种情况下TransitionManager会使用默认的AutoTransition。我们将会在下一节讨论建立自定义的transitions。但是在这之前我们要知道现在布局中是缺少内容的。
我们已经看到布局已经被re-inflated了,但是还没有机会添加任何内容。你可以直接在xml布局中添加,但这不是一个明智的决定 -鉴于重用与国际化两个原因, 一个布局应该是与内容独立的。因此,我们在 values/strings.xml中定义了如下的strings:
<?xml version="1.0" encoding="utf-8"?> <resources> ... <string name="sample_story_1_content">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus et leo pulvinar, egestas est sit amet, fringilla nisl....</string> <string name="sample_story_2_content">Suspendisse sapien metus, ornare ac metus a, ultrices semper risus. Suspendisse auctor adipiscing tortor. Nunc luctus,...</string> ... </resources>
同时我们也在相应的资源文件夹添加了 sample1.jpeg, sample2.jpeg 之类的图片。
下面的方法将为由layout创建的ViewGroup设置相应的内容:
private void addContentToViewGroup(ViewGroup viewGroup) { if (mItem != null) { TextView contentTextView = (TextView) viewGroup.findViewById(R.id.story_content); if(contentTextView != null) { contentTextView.setText(getResources().getText(mItem.contentResourceId)); } TextView titleTextView = (TextView) viewGroup.findViewById(R.id.story_title); if(titleTextView != null) { titleTextView.setText(mItem.title); } ImageView imageView = (ImageView) viewGroup.findViewById(R.id.story_image); if(imageView != null) { imageView.setImageResource(mItem.imageResourceId); } } }
其中mItem是StoryItem的实例:
public static class StoryItem { public String id; public String title; public int contentResourceId; public int imageResourceId; public StoryItem(String id, String title, int contentResourceId, int imageResourceId) { this.id = id; this.title = title; this.contentResourceId = contentResourceId; this.imageResourceId = imageResourceId; } @Override public String toString() { return this.title; } }
这是安卓中找到view并动态设置内容的标准模式。
为了把内容插入到布局中我们使用了Scene对象的一个属性-EnterAction。这是一个Runnable,在布局被inflated之后transition 执行之前运行 - 加载内容的理想时机。
// Just before the transition starts, ensure that the content has been loaded scene.setEnterAction(new Runnable() { @Override public void run() { addContentToViewGroup(container); } });
在创建Scene的循环中添加上面的代码,可以保证内容的加载是在transition 开始之前,现在你运行app你会看到内容现在是完全加载了的:
目前为止,我们的transition 完全是自动的-我们没有指定transition 该如何动画的任何东西,显然这是我们想要控制的。
我们从重新定义AutoTransition的效果开始(注意下面transitionSet部分的代码和AutoTransition非常类似),这样你就能看到它是由什么构成的:
private void performTransitionToScene(Scene scene) { Fade fadeOut = new Fade(Fade.OUT); ChangeBounds changeBounds = new ChangeBounds(); Fade fadeIn = new Fade(Fade.IN); TransitionSet transitionSet = new TransitionSet(); transitionSet.setOrdering(TransitionSet.ORDERING_SEQUENTIAL); transitionSet.addTransition(fadeOut) .addTransition(changeBounds) .addTransition(fadeIn); TransitionManager.go(scene, transitionSet); }
这个方法将从当前的scene 过渡到一个新的scene ,使用了一个TransitionSet,它结合了 fade-out,bounds change,fade-in三种过渡效果(transition)。
一个TransitionSet是一个有序的transition集合,你可以用setOrdering()方法设置这些transition是同时发生还是依次发生。为了重建AutoTransition的效果,应该为依次发生。
TransitionManager的静态方法go()可以占有两个参数,除了Scene参数之外,还可以有个Transition参数。
我们把这个方法
TransitionManager.go(sceneList.get(tab.getPosition()));
替换成:
performTransitionToScene(sceneList.get(tab.getPosition()));
如果你现在运行app,你不会看到有任何变化,我们只是自己重新实现了一遍默认的automatic transition 。这样做的好处是对transition 有更多的控制权。比如,我们可以在每个transition上设置一个持续时间。
private void performTransitionToScene(Scene scene) { Fade fadeOut = new Fade(Fade.OUT); ChangeBounds changeBounds = new ChangeBounds(); Fade fadeIn = new Fade(Fade.IN); fadeOut.setDuration(1000); changeBounds.setDuration(1000); fadeIn.setDuration(1000); changeBounds.setInterpolator(new BounceInterpolator()); TransitionSet transitionSet = new TransitionSet(); transitionSet.setOrdering(TransitionSet.ORDERING_SEQUENTIAL); transitionSet.addTransition(fadeOut) .addTransition(changeBounds) .addTransition(fadeIn); TransitionManager.go(scene, transitionSet); }
上面的代码片段还引入了一个叫做Interpolator的东西。这是一个用于匹配 transition 时间与空间的对象。框架本身提供了一些interpolator,包括AccelerateDecelerateInterpolator与AnticipateOvershootInterpolator,这里我们使用BounceInterpolator:
跟安卓的许多其它东西一样,Transition和TransitionManager对象都可以用xml来指定。首先我们看看如何用xml重新创建前面创建的bounce transition。
在transition目录创建一个新的xml资源,取名叫 bouncy_auto_transition.xml,root元素应该是transitionSet:
<?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:transitionOrdering="sequential" > <fade android:fadingMode="fade_out" android:duration="1000" /> <changeBounds android:interpolator="@android:interpolator/bounce" android:duration="1000" /> <fade android:fadingMode="fade_in" android:duration="1000" /> </transitionSet>
可以看到这个XML 的结构很容易看懂- transitionOrdering是transitionSet元素的一个属性。然后每个transition里面则是用fadingMode、duration和interpolator定义细节。
为了使用这个新的transition,我们更新performTransitionToScene()方法的代码:
TransitionInflater inflater = TransitionInflater.from(StoryDetailActivity.this); Transition transition = inflater.inflateTransition(R.transition.bouncey_auto_transition); TransitionManager.go(scene, transition);
这里我们创建了一个TransitionInflater(使用StoryDetailActivity.this是因为我们需要context 并且这是在一个内部类中),用它来inflate 一个Transition对象。
然后我们使用了和前面同样的静态方法go()。
如果你现在运行app,也没有任何变化,因为我们只是把java代码替换成了XML 资源文件。
xml资源文件的真正强大之处在于我们可以创建一个TransitionManager。在transition 目录中创建一个叫做 story_transition_manager.xml 的文件。记住根元素是transitionManager。
语法还是很简单 - 一个transition manager由一组transition组成 - 每个transition都指定了从哪个scene 过渡而来,过渡到哪个scene 以及使用哪个transition 对象。
<?xml version="1.0" encoding="utf-8"?> <transitionManager xmlns:android="http://schemas.android.com/apk/res/android"> <transition android:fromScene="@layout/content_scene_00" android:toScene="@layout/content_scene_01" android:transition="@transition/simultaneous_bounce_transition" /> <transition android:fromScene="@layout/content_scene_00" android:toScene="@layout/content_scene_02" android:transition="@transition/bouncy_auto_transition" /> ... </transitionManager>
这里我们引入了第二个transition 类型simultaneous_bounce_transition
<?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:transitionOrdering="together"> <fade android:fadingMode="fade_out" android:duration="500" /> <changeBounds android:interpolator="@android:interpolator/bounce" android:startDelay="250" android:duration="1500" /> <fade android:fadingMode="fade_in" android:startDelay="750" android:duration="1000" /> </transitionSet>
你不需要在transition manager 中列举出所有的scene-scene组合- 如果没有找到一个合适的transition ,则会使用默认的AutoTransition。
为了使用这个新的transition manager,我们在activity中添加一个成员变量来存储对它的引用:
/** * The transition manager, inflated from XML */ private TransitionManager mTransitionManager;
然后在onCreate()中创建一个inflater同时创建 transition manager:
// Build the transition manager TransitionInflater transitionInflater = TransitionInflater.from(this); mTransitionManager = transitionInflater.inflateTransitionManager(R.transition.story_transition_manager, container);
这样performTransitionToScene()方法就变成了:
private void performTransitionToScene(Scene scene) { mTransitionManager.transitionTo(scene); }
如果现在你运行app就会发现某些transition跟原来的bouncy transition一样,而某些则是新的simultaneous bounce transition:
安卓中的动画在最近的版本增加了许多东西,而android.transition framework 的引入让其进入了一个更高的层面。transition为复杂的动画提供了一个简单的入口。它还提供了xml的使用方式,大大简化了代码与关注点的独立性。
虽然这篇文章覆盖了这个新框架的所有主要部分,但仍然有许多还未考虑的地方 - 最好看看文档然后尝试能做出些什么动画体验来。
就如前面提到的,所有的代码都在github 的github.com/ShinobiControls/bitesize-kitkat。如果你有什么问题欢迎提交pull-request,在下面留言或者在twitter上找我– @iwantmyrealname。