Transitions Framework 的一篇教程

原文: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 得到。可以查看到目前为止发布的所有文章的 目录 。

Scenes 与 TransitionManager

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切换的同时内容会自动播放动画:

Transition No Content

你都还没有指定动画该如何过渡的任何东西,而在这种情况下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你会看到内容现在是完全加载了的:

Simple Transitions

用TransitionSet自定义transition

目前为止,我们的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 With Bounce

使用XML资源文件来定义Transition

跟安卓的许多其它东西一样,Transition和TransitionManager对象都可以用xml来指定。首先我们看看如何用xml重新创建前面创建的bounce transition。

创建一个Transition XML资源

在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 资源文件。

创建一个TransitionManager 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:

Transitions 2 Types

总结

安卓中的动画在最近的版本增加了许多东西,而android.transition framework 的引入让其进入了一个更高的层面。transition为复杂的动画提供了一个简单的入口。它还提供了xml的使用方式,大大简化了代码与关注点的独立性。

虽然这篇文章覆盖了这个新框架的所有主要部分,但仍然有许多还未考虑的地方 - 最好看看文档然后尝试能做出些什么动画体验来。

就如前面提到的,所有的代码都在github 的github.com/ShinobiControls/bitesize-kitkat。如果你有什么问题欢迎提交pull-request,在下面留言或者在twitter上找我– @iwantmyrealname