Android过渡动画(Transition Animation)

本篇博客记录一下Android中过渡动画的用法。
过渡动画是Android 4.4引入的新的动画框架,它本质上仍是属性动画,
但对属性动画做了一层封装,以方便开发者实现Activity或者View的过渡动画效果。

和属性动画相比,过渡动画最大的不同是需要为动画前后准备不同的布局,
并通过对应的API实现两个布局的之间过渡动画。

不过相对而言,过渡动画比较吃内存,据传闻传闻,
在针对低内存设备的Android Go版本中,部分过渡动画会被砍掉。


一、简单示例
我们先来看看Transition动画的简单使用示例。

如下图所示,我们的Activity中定义了3个ImageView。
现在,我们准备将这些试图顺时针旋转一下。

如果使用补间动画或属性动画,为了实现这个需求,
我们需要写3个位置移动动画,然后分别应用到这三个View上面。
若使用过渡动画,整个实现过程则会简单的多。

准备工作
我们首先定义一下Activity对应的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/begin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始动画"/>

    <!--做动画的父布局-->
    <FrameLayout
        android:id="@+id/rootView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <!--scene1为动画的初始布局-->
        <include layout="@layout/scene1"/>
    </FrameLayout>
</LinearLayout>

定义启始与结束帧
在layout目录下定义初始时的视图布局文件scene1.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/p_1"
        android:layout_centerInParent="true"/>

    <ImageView
        android:id="@+id/image2"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/p_2"
        android:layout_below="@id/image1"
        android:layout_alignParentLeft="true"/>

    <ImageView
        android:id="@+id/image3"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/p_3"
        android:layout_below="@id/image1"
        android:layout_alignParentRight="true"/>

</RelativeLayout>

然后,我们还需要定义结束时的视图布局文件scene2.xml,
注意视图中View的id与scene1.xml中一一对应:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image3"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/p_3"
        android:layout_centerInParent="true"/>

    <ImageView
        android:id="@+id/image1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/p_1"
        android:layout_below="@id/image3"
        android:layout_alignParentLeft="true"/>

    <ImageView
        android:id="@+id/image2"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/p_2"
        android:layout_below="@id/image3"
        android:layout_alignParentRight="true"/>

</RelativeLayout>

定义过渡效果
在过渡动画框架中,过渡效果均是继承Transition类。
框架内置了一些常用的效果,比如幻灯片Slide,淡入淡出 Fade等。
由于我们这里3个View涉及的都是位置的变化,
我们可以直接使用框架内置的动画效果ChangeBounds(实际上是变换视图边界的坐标)。

实际的使用

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final ViewGroup rootView = findViewById(R.id.rootView);

        Button button = findViewById(R.id.begin);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    //定义结束Scene
                    Scene scene2 = Scene.getSceneForLayout(rootView, R.layout.scene2, MainActivity.this);

                    //利用TransitionManager进行变换
                    TransitionManager.go(scene2, new ChangeBounds());
                }
            }
        });
    }
}

至此,我们已经可以看出过渡动画的基本使用方式。

如上面的示意图所示:
首先,利用布局文件定义了一个视图树的两种状态(类似于开始帧和结束帧),这些状态被称为Scene。
Scene定义了页面的当前状态信息,Scene的实例化一般通过静态工厂方法实现:

public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) {
    .............
}

然后,选定需要使用的动画效果,例如ChangeBounds等,
它能够处理View本身大小、位置改变,描述这些变化效果的对象就被成为Transition。
Transition定义了界面之间切换的动画信息,在使用TransitionManager时,
如果没有指定使用哪个Transition,那么会使用默认的AutoTransition。
AutoTranstion的效果就是先隐藏,后移动,最后显示:

public class AutoTransition extends TransitionSet {
    ...........
    private void init() {
        setOrdering(ORDERING_SEQUENTIAL);
        addTransition(new Fade(Fade.OUT)).
                addTransition(new ChangeBounds()).
                addTransition(new Fade(Fade.IN));
    }
}

最后,通过TransitionManager的接口,开启整个视图的动画转换即可。
TransitionManager就是控制Scene之间切换的控制器。

二、场景Scene
从前文的代码我们知道,Scene记录了一个视图树中所有View的属性值。
前面我们使用如下接口构造Scene:

public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) {
    .............
}

除此之外,我们也可以利用构造函数直接创建Scene:

public Scene(ViewGroup sceneRoot, View layout) {
    ......
}

可以看到,无论使用哪种方式创建Scene,都必须指定sceneRoot。
sceneRoot就是Scene对应视图树的根View。

当过渡动画开始时,sceneRoot中与初始Scene有关的View都会被remove,
即初始Scene中的View必须都是sceneRoot的子View;
在动画的结束时,sceneRoot重新添加结束Scene相关的View,
即结束Scene中的View必须都是sceneRoot的子View,或没有ParentView的View,
否则addView时会报错。

三、过渡Transition
Transition针对初始和结束场景对应视图树中View的属性,
例如width、height、position等,定义了变化时的过度效果。
Android目前内置了ChangeBounds、Fade等。

3.1、指定目标
前面示例中,Transition直接作用在整个视图树,
即对视图树上所有的View均生效。

如果不想将动画效果应用到所有的View上,
可以利用Transition的addTarget或removeTarget指定生效的View。
例如前面的例子,可以修改为:

.................
Scene scene2 = Scene.getSceneForLayout(rootView,
        R.layout.scene2, MainActivity.this);
Transition transition = new ChangeBounds();
transition.addTarget(R.id.image1);
TransitionManager.go(scene2, transition);
.................

此时,只有image1对应视图在变化时有动画效果。

3.2、延迟动画
之前已经提到过,过渡动画实际上也是一种属性动画。
这一点从TransitionManager提供的延迟动画接口就可以看出来。
使用延迟动画接口时,不需要定义结束帧对应的Scene。
仍然使用前文的示例代码:

ChangeBounds changeBounds = new ChangeBounds();
changeBounds.setDuration(3000);
//开启延迟动画
TransitionManager.beginDelayedTransition(rootView, changeBounds);

//可以直接修改rootView中子View的属性
//当动画结束后,子View的属性就会被修改到指定值

View image1 = findViewById(R.id.image1);
ViewGroup.LayoutParams params1 = image1.getLayoutParams();
params1.height = 200;
params1.width = 200;
image1.setLayoutParams(params1);

View image2 = findViewById(R.id.image2);
ViewGroup.LayoutParams params2 = image2.getLayoutParams();
params2.height = 400;
params2.width = 400;
image2.setLayoutParams(params2);

当TransitionManager调用beginDelayedTransition后,系统会保存当前视图树的状态;
接着在代码中,我们修改了子View的属性;
在下一次绘制时,系统会比对视图树当前的状态和之前保存的状态,
然后系统就会运行过渡动画,并按照新的状态绘制View。

3.3、自定义Transition
这部分我们来看看如何自定义Transition,直接上例子:

    //我定义了一个起始和结束时,View的高度发生变化时,就会旋转的动画效果
    @TargetApi(19)
    private class RotateWhenHeightChangeTransition extends Transition {
        private static final String PROP_NAME_TRANSITION_HEIGHT = "zhang:jian:just:for:test";

        //首先必须复写captureStartValues和captureEndValues
        //如同函数名,这里主要记录我们感兴趣的数据
        //按照键值对的形式存入transitionValues.values

        //记录开始Scene中View的高度
        @Override
        public void captureStartValues(TransitionValues transitionValues) {
            transitionValues.values.put(
                    PROP_NAME_TRANSITION_HEIGHT, transitionValues.view.getLayoutParams().height);
        }

        //记录结束Scene中View的高度
        @Override
        public void captureEndValues(TransitionValues transitionValues) {
            transitionValues.values.put(
                    PROP_NAME_TRANSITION_HEIGHT, transitionValues.view.getLayoutParams().height);
        }

        @Override
        public Animator createAnimator(ViewGroup sceneRoot,
                                       TransitionValues startValues, TransitionValues endValues) {
            if (startValues == null || endValues == null) {
                return null;
            }

            //比对开始和结束时,高度是否发生变化
            int startHeight = (int)startValues.values.get(PROP_NAME_TRANSITION_HEIGHT);
            int endHeight = (int) endValues.values.get(PROP_NAME_TRANSITION_HEIGHT);
            //若发生变化,则创建属性动画对应的Animator
            if (startHeight != endHeight) {
                return ObjectAnimator
                        .ofFloat(endValues.view, "rotation", 0, 360)
                        .setDuration(2000);
            }

            return null;
        }
    }

定义好后,就可以像系统内置的Transition那样,使用RotateWhenHeightChangeTransition了:

.......
//更改一下scene2对应的View的高度
//可以发现高度变化的View才会有旋转动画
Scene scene2 = Scene.getSceneForLayout(rootView,
        R.layout.scene2, MainActivity.this);
TransitionManager.go(scene2, new RotateWhenHeightChangeTransition());
........

四、总结
至此,我们已经明白过渡动画的使用和自定义,
应该可以满足一般的工作需求。

猜你喜欢

转载自blog.csdn.net/gaugamela/article/details/79262529