一 概述
Scene 是Android 19 引入的转换框架中一个场景api,帮我们友好的创建开始布局Scene和结束布局Scene,有了开始Scene和结束Scene,运用Transition框架来实现带有动画的场景切换。举个例子,从A布局切换到B布局,一般情况下处理是View.GONE,View.VISIBLE,但是这样太生硬了,没有一点过度效果。那么Android的Transition框架就可以完美的解决切换场景带来的生硬视觉感受。
其中Scene是一个容器,就是放置你定义的布局,而真正去做场景之间切换这个动作是Transition框架中TransitionManager 调用其中go方法或者transitionTo方法完成场景之间切换,而真正创建具体动画交由Transition子类来完成,开始动画交给Transition来执行。
二 使用
这里一共有4个场景,这里先说前3个。每次切换都带有移动效果。在切换到第三个场景时,单独给第三个场景中TextView添加了淡入和淡出动画效果。
那么如果让我们去实现这样一个场景切换,可能会想到在一个布局中给不同的元素设置不同的动画,还得监听每个动画完成后显示第二个场景中的元素。这样写出来很难阅读和维护,如果再加一个元素,又得监听以及显示和隐藏。
那么如何实现前三个场景切换呢?其实这三个场景对应三个layout.xml。每一个layout.xml对应一个Scene,各自之间不耦合,至于具体动画创建和偏移计算交给Transition子类来处理,TransitionManager 只是用于做控制流程。
上述中gif图代码如下:
创建布局文件
<!--默认布局-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
>
<RadioGroup
...>
<RadioButton
android:id="@+id/select_scene_1"
...
>
<RadioButton
android:id="@+id/select_scene_2"
...>
<RadioButton
android:id="@+id/select_scene_3"
...
>
<RadioButton
android:id="@+id/select_scene_4"
...>
</RadioGroup>
<FrameLayout
android:id="@+id/scene_root"
...
>
<include layout="@layout/scene1"/>
</FrameLayout>
</LinearLayout>
<!--场景一 布局文件-->
<RelativeLayout
android:id="@+id/container"
...>
<矩形
android:id="@+id/transition_square"
/>
<箭头
android:id="@+id/transition_image"
android:layout_below="@id/transition_square"
/>
<圆形
android:id="@+id/transition_oval"
android:layout_below="@id/transition_image"
/>
</RelativeLayout>
<!--场景二 布局文件-->
<RelativeLayout
android:id="@+id/container"
...
>
<View
android:id="@+id/transition_square"
android:layout_alignParentBottom="true"/>
<ImageView
android:id="@+id/transition_image"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"/>
<ImageView
android:id="@+id/transition_oval"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
场景三布局文件这里省略。其实布局一样只是各个View中的位置不一样,场景三多了一个TextView,这里暂且先说场景一和场景二。从场景一和场景二可以发现两个场景id是一样的,只是位置不一样。其实场景切换中匹配规则除了id匹配还有如下匹配规则:
- instance 匹配同一引用
- transitionName 匹配同一transitionName
- itemId 匹配ListView中adapter id
接下来就是代码创建Scene,调用TransitionManager.go()方法开启场景切换。
创建场景Scene1
ViewGroup mSceneRoot = (ViewGroup) view.findViewById(R.id.scene_root);
Scene mScene1 = new Scene(mSceneRoot, (ViewGroup) mSceneRoot.findViewById(R.id.container));
创建场景Scene2
Scene mScene2 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene2, getActivity());
开始动画
...
case R.id.select_scene_1: {
TransitionManager.go(mScene1);
break;
}
case R.id.select_scene_2: {
TransitionManager.go(mScene2);
break;
}
...
这样就实现上面gif图场景一到场景二切换的动画了,场景三这里同理。上面给出创建场景,有两种方式。
- 从当前位置创建Scene
- 通过Scene.getSceneForLayout()方法创建Scene
Scene.getSceneForLayout() 参数介绍如下:
- sceneRoot 表示从什么地方开始切换场景。
- layoutId 切换到什么场景的布局文件,比如上述例子中生成Scene2实例,layoutId 就是场景2中的布局。
- context 上下文
上述简单的例子是通过TransitionManager.go()触发动画,go()方法中其实设置了默认转换动画
private void init() {
setOrdering(ORDERING_SEQUENTIAL);
addTransition(new Fade(Fade.OUT)).
addTransition(new ChangeBounds()).
addTransition(new Fade(Fade.IN));
}
设置了一个changeBounds,和Fade转换效果。
类似于ChangeBounds类的还有以下几种,他们都是继承Transiton类
- ChangeBounds检测view的位置边界创建移动和缩放动画
- ChangeTransform检测view的scale和rotation创建缩放和旋转动画
- ChangeClipBounds检测view的剪切区域的位置边界,和ChangeBounds类似。不过ChangeBounds针对的是view而ChangeClipBounds针对的是view的剪切区域(setClipBound(Rect rect) 中的rect)。如果没有设置则没有动画效果
- ChangeImageTransform检测ImageView(这里是专指ImageView)的尺寸,位置以及ScaleType,并创建相应动画。
- Fade,Slide,Explode这三个都是根据view的visibility的不同分别创建渐入,滑动,爆炸动画。
上述通过代码实现场景转换动画,下面通过xml方式定义一组动画集合。在res/transition/创建xml文件
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds />
<fade android:fadingMode="fade_in_out"/>
</transitionSet>
xml使用
transition = TransitionInflater.from(this).inflateTransition(R.transition.changebounds_fadein_together);
isSwitch = !isSwitch;
TransitionManager.go(isSwitch ? scene2 : scene1, transition);
前面说了场景一和场景二的创建和使用,那么场景三也就so easy 了,可以发现场景三中多一个TextView,此TextView需要在切换场景时,淡入淡出,怎么实现呢?
其实可以通过TransitionInflater 中inflateTransitionManager方法实现。
- 定义scene3_transition_manager.xml,
指定场景布局文件,和切换动画。
<transitionManager xmlns:android="http://schemas.android.com/apk/res/android">
<transition
android:toScene="@layout/scene3"
android:transition="@transition/changebounds_fadein_together"/>
</transitionManager>
- 定义changebounds_fadein_together.xml,
指定动画效果和需要新加入的Viewid。此时为场景三中新加入TextView Id
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds />
<fade android:fadingMode="fade_in_out">
<targets>
<target android:targetId="@id/transition_title" />
</targets>
</fade>
</transitionSet>
其中定义的target标签 targetId 表示只针对于这个id,还有和它相反的
excludeId 除了这个id。(excludeId 仅在api21上才可以)
创建场景Scene3
Scene mScene3 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene3, getActivity());
加载xml中定义的转换效果
TransitionManager mTransitionManagerForScene3 =TransitionInflater.from(getActivity())
.inflateTransitionManager(R.transition.scene3_transition_manager, mSceneRoot)
调用
mTransitionManagerForScene3.transitionTo(mScene3)
上述例子中是针对多个场景切换实现转换动画,
那么有时候没有多个场景切换,只想改变其中某一个场景下的某一个View属性来实现过度动画,
可以使用一下api。
TransitionManager.beginDelayedTransition(ViewGroup sceneRoot)
beginDelayedTransition原理是通过代码改变view的属性,然后通过之前介绍的ChangeBounds等类分析start scene和end Scene不同来创建动画。
以下例子实现在一个场景中特定的View放大效果,其实也是上面gif图的场景4了,具体效果,文末给出代码链接,可自行查看。
TransitionManager.beginDelayedTransition(mSceneRoot);
View square = mSceneRoot.findViewById(R.id.transition_square);
ViewGroup.LayoutParams params = square.getLayoutParams();
int newSize=getResources().getDimensionPixelSize(R.dimen.square_size_expanded);
params.width = newSize;
params.height = newSize;
square.setLayoutParams(params);
三 原理
就拿上述中定义的两个场景Scene1 和 Scene2来说, Scene 只是用来保存当前场景布局,而真正去创建动画和开始动画的是Transition,TransitionManager 只是用来做控制流程的。
假设
Scene1-->Scene2
当代码调用TransitionManager.go(mScene2)时执行流程
从设置的mSceneRoot 开始,遍历Scene1中视图树,存储每次遍历的View在自己父View中的位置,以及该View中的id作为开始动画时位置和条件,当保存完毕,删除mSceneRoot 中存在的Scene1添加Scene2监听视图树的绘制,当绘制完毕遍历当前已添加的Scene2中视图树,存储每次遍历的View在自己父View中的位置,以及该View中的id作为结束动画位置和条件。当动画的开始位置和动画结束位置已确定,那么创建动画交给Transition子类,上述例子中用的是Transition的子类 ChangeBounds 。当通过结束位置和开始位置创建动画完毕后,最终通过Transition中runAnimators开启动画。
参考链接: