概述
Android 4.4.2 (API level 19)引入Transition框架,之后很多APP上都使用该框架做出很酷炫的效果,如 Google Play Newsstand app

在app中适当得使用上Transition能带来较好的用户体验,视频中介绍了该框架的基本使用以及其中核心的一些类和方法,只有学会这些基本的API才能在之后的Activity/Fragment过渡定制一些自己想要的效果。
先看官网的一张关系图

图中有三个核心的类,分别是Scene、Transition和TransitionManager,下面对这个三个核心类展开分析。
Scene

Scene场景,用于保存布局中所有View的属性值,创建Scene的方式可以通过getSceneForLayout方法
比如:
1
2
| mScene0 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene0, getContext());
mScene1 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene1, getContext());
|
也可以直接new Scene(ViewGroup sceneRoot, View layout)
1
2
3
4
| View view0 = inflater.inflate(R.layout.scene0, container, false);
View view1 = inflater.inflate(R.layout.scene1, container, false);
mScene0 = new Scene(mSceneRoot, view0);
mScene1 = new Scene(mSceneRoot, view1);
|
两种方式都需要传SceneRoot,即该场景的根节点。
Transition

Transition过渡动画,前面创建了两个场景,分别保存了视图的一些属性,比如Visibility、position等,Transition就是对于这些属性值的改变定义过渡的效果。从上图可以看到系统内置了一些常用的Transition,Transition的创建可以通过加载xml,如:
res/transition/fade_transition.xml
1
| <fade xmlns:android="http://schemas.android.com/apk/res/android" />
|
然后在代码中:
1
2
3
| Transition mFadeTransition =
TransitionInflater.from(this).
inflateTransition(R.transition.fade_transition);
|
或者直接在代码中:
1
| Transition mFadeTransition = new Fade();
|
TransitionManager
TransitionManeger用于将Scene和Transition联系起来,它提供了一系列的方法如setTransition(Scene fromScene, Scene toScene, Transition transition)指明起始场景和结束场景、他们的过渡动画是什么,go(Scene scene, Transition transition),到指定的场景所使用的过渡动画是什么,beginDelayedTransition(ViewGroup sceneRoot, Transition transition),在当前场景到下一帧的过渡效果是什么。比如这里使用go()方法,效果:

注意这里两个Scene中红绿两个方块除了位置和大小不一样,id是一致的,transition记录下两个Scene前后属性值,根据属性值的改变执行过渡动画,默认情况下对SceneRoot下的所有View执行动画效果,我们可以通过Transition.addTarget和removeTarget方法选择性添加或移除执行动画的View。
常用API
有时候我们只想改变当前已展示的视图层级中View的状态,可以通过beginDelayedTransition实现,下面列举系统内置的Transition的使用。
AutoTransition
AutoTransition默认的动画效果,对应xml tag为autoTransition
其实是以下几个动画组合顺序执行:
1
2
3
4
5
6
| <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>
|
在代码中使用:
1
2
3
4
5
6
| TransitionManager.beginDelayedTransition(mRoot, new AutoTransition());
if (mTextView.getVisibility() != View.VISIBLE) {
mTextView.setVisibility(View.VISIBLE);
} else {
mTextView.setVisibility(View.GONE);
}
|

ChangeBounds
ChangeBounds对应xml tag为changeBounds,根据前后布局界限的变化执行动画
1
2
3
4
5
6
7
8
| TransitionManager.beginDelayedTransition(mRoot, new ChangeBounds());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mTarget.getLayoutParams();
if ((lp.gravity & Gravity.START) == Gravity.START) {
lp.gravity = Gravity.BOTTOM | Gravity.END;
} else {
lp.gravity = Gravity.TOP | Gravity.START;
}
mTarget.setLayoutParams(lp);
|

ChangeClipBounds
ChangeClipBounds对应xml tag为changeClipBounds,作用对象:View的getClipBounds()值
1
2
3
4
5
6
7
| Rect BOUNDS = new Rect(20, 20, 100, 100);
TransitionManager.beginDelayedTransition(mRoot, new ChangeClipBounds());
if (BOUNDS.equals(ViewCompat.getClipBounds(mImageView))) {
ViewCompat.setClipBounds(mImageView, null);
} else {
ViewCompat.setClipBounds(mImageView, BOUNDS);
}
|

对应xml tag为changeImageTransform,作用对象:ImageView的matrix
1
2
| TransitionManager.beginDelayedTransition(mRoot, new ChangeImageTransform());
mImageView.setScaleType(ImageView.ScaleType.XXX);
|

对应xml tag为changeScroll,作用对象:View的scroll属性值
1
2
| TransitionManager.beginDelayedTransition(mRoot, new ChangeScroll());
mTarget.scrollBy(-100, -100);
|

对应xml tag 为changeTransform,作用对象:View的scale和rotation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| TransitionManager.beginDelayedTransition(mRoot, new ChangeTransform());
if (mContainer2.getChildCount() > 0) {
mContainer2.removeAllViews();
showRedSquare(mContainer1);
} else {
mContainer1.removeAllViews();
showRedSquare(mContainer2);
mContainer2.getChildAt(0).setRotation(45);
}
private void showRedSquare(FrameLayout container) {
final View view = LayoutInflater.from(getContext())
.inflate(R.layout.red_square, container, false);
container.addView(view);
}
|

Explode
对应xml tag为explode,作用对象:View的Visibility
1
2
3
4
5
| TransitionManager.beginDelayedTransition(mRoot, new Explode());
int vis = mViews.get(0).getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
for (View view : mViews) {
view.setVisibility(vis);
}
|

Fade
对应xml tag为fade,作用对象:View的Visibility
可以在初始化时指定IN或者OUT分别对应淡入和淡出,也可以通过fade.setMode方法设置,若不指定默认为淡入淡出效果
1
2
3
4
5
| TransitionManager.beginDelayedTransition(mRoot, new Fade());
int vis = mViews.get(0).getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
for (View view : mViews) {
view.setVisibility(vis);
}
|

Slide
对应xml tag为slide,作用对象:View的Visibility
可以初始化时传入Gravity.XX,也可以通过slide.setSlideEdge方法设置,默认方向为Gravity.BOTTOM
1
2
3
4
5
| TransitionManager.beginDelayedTransition(mRoot, new Slide());
int vis = mViews.get(0).getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
for (View view : mViews) {
view.setVisibility(vis);
}
|

TransitionSet
对应xml tag为transitionSet
可以在代码中创建transitionSet如:
1
2
3
4
5
6
7
8
9
10
11
12
| mTransition = new TransitionSet();
mTransition.addTransition(new ChangeImageTransform());
mTransition.addTransition(new ChangeTransform());
TransitionManager.beginDelayedTransition(mOuterFrame, mTransition);
if (mInnerFrame.getChildCount() > 0) {
mInnerFrame.removeAllViews();
addImageView(mOuterFrame, ImageView.ScaleType.CENTER_CROP, mPhotoSize);
} else {
mOuterFrame.removeViewAt(1);
addImageView(mInnerFrame, ImageView.ScaleType.FIT_XY,
FrameLayout.LayoutParams.MATCH_PARENT);
}
|
也可以通过加载xml布局创建transitionSet:
xml布局长这样:
1
2
3
4
5
6
| <?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="together">
<changeImageTransform/>
<changeTransform/>
</transitionSet>
|
通过transitionOrdering属性设置动画执行的顺序,together表示同时执行,sequential表示顺序执行,在代码中可以调用TransitionSet的setOrdering(int)方法,属性值传ORDERING_SEQUENTIAL或者ORDERING_TOGETHER
在代码中:
1
2
3
4
5
6
7
8
9
10
| mTransition = (TransitionSet) TransitionInflater.from(getContext()).inflateTransition(R.transition.transition);
TransitionManager.beginDelayedTransition(mOuterFrame, mTransition);
if (mInnerFrame.getChildCount() > 0) {
mInnerFrame.removeAllViews();
addImageView(mOuterFrame, ImageView.ScaleType.CENTER_CROP, mPhotoSize);
} else {
mOuterFrame.removeViewAt(1);
addImageView(mInnerFrame, ImageView.ScaleType.FIT_XY,
FrameLayout.LayoutParams.MATCH_PARENT);
}
|
这里结合changeImageTransform和changeTransform,效果如下:

PathMotion

Transition的辅助工具,以path的方式指定过渡效果,两个具体实现类ArcMotion和PatternPathMotion,看下ArcMotion的效果
1
2
3
4
5
6
7
8
9
10
| mTransition = new AutoTransition();
mTransition.setPathMotion(new ArcMotion());
TransitionManager.beginDelayedTransition(mRoot, mTransition);
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mTarget.getLayoutParams();
if ((lp.gravity & Gravity.START) == Gravity.START) {
lp.gravity = Gravity.END | Gravity.BOTTOM;
} else {
lp.gravity = Gravity.START | Gravity.TOP;
}
mTarget.setLayoutParams(lp);
|

它的运动轨迹是条曲线,有兴趣的可以研究下它的实现算法,在源码中有个很萌的图如下:

自定义Transition
除了系统内置的Transition,我们还可以自定义Transition效果,需要继承Transition
1
2
3
4
5
6
7
8
9
10
| public class CustomTransition extends Transition {
@Override
public void captureStartValues(TransitionValues values) {}
@Override
public void captureEndValues(TransitionValues values) {}
@Override
public Animator createAnimator(ViewGroup sceneRoot,
TransitionValues startValues,
TransitionValues endValues) {}
}
|
其工作原理是在captureStartValues和captureEndValues中分别记录View的属性值,官网建议确保属性值不冲突,属性值的命名格式参考:
1
| package_name:transition_name:property_name
|
在createAnimator中创建动画,对比属性值的改变执行动画效果,如自定义修改颜色动画效果:

在两个Scene中使用自定义过渡动画,效果如下:

Note
1.Android 版本在4.0(API Level 14)到4.4.2(API Level 19)使用Android Support Library’s
2.对于 SurfaceView可能不起效果,因为SurfaceView的实例是在非UI线程更新的,因此会造成和其他视图动画不同步。
3.某些特定的转换类型在应用到TextureView时可能不会产生所需的动画效果。
4.继承自AdapterView的如ListView,与该框架不兼容。
5.不要对包含文本的视图的大小进行动画
Thanks to
Google Demo
Github Demo传送门
作者:HuYounger
本文转载自:http://rkhcy.github.io
下篇
在上篇笔记中对于Transition的框架和常用的API使用进行了分析,Transition最常用的是在界面过渡方面,本文继续学习Transition在界面过渡上的使用。在界面过渡上,Transition分为不带共享元素的Content Transition和带共享元素的ShareElement Transition。
Content Transition
先看下content transition的一个例子,在Google Play Games上的应用:

在经过学习后我们也可以设计出类似的效果,首先需要了解在界面过渡中涉及到的一些重要方法,从ActivtyA调用startActivity方法唤起ActivityB,到ActivityB按返回键返回ActivityA涉及到与Transition有关的方法


因此,只要我们在对应的方法中设置了Transition就可以了。在默认没有设置对应Transition的情况下,Material-theme应用的exitTransition为null,enterTransition为Fade,如果reenterTransition和returnTransition未设定,则分别对应exitTransition和enterTransition。
使用
在style中添加android:windowContentTransitions 属性启用窗口内容转换(Material-theme应用默认为true),指定该Activity的Transition
1
2
3
4
5
6
7
8
9
| <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- enable window content transitions -->
<item name="android:windowContentTransitions">true</item>
<!-- specify enter and exit transitions -->
<!-- options are: explode, slide, fade -->
<item name="android:windowEnterTransition">@transition/change_image_transform</item>
<item name="android:windowExitTransition">@transition/change_image_transform</item>
</style>
|
也可以在代码中指定
1
2
3
4
5
6
| // inside your activity (if you did not enable transitions in your theme)
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
// set an enter transition
getWindow().setEnterTransition(new Explode());
// set an exit transition
getWindow().setExitTransition(new Explode());
|
然后启动Acticity
1
2
| startActivity(intent,
ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
|
例子
这里在代码中指定ActivityA的exitTransition:
1
2
3
4
5
6
| private void setupTransition() {
Slide slide = new Slide(Gravity.LEFT);
slide.setDuration(1000);
slide.setInterpolator(new FastOutSlowInInterpolator());
getWindow().setExitTransition(slide);
}
|
使用xml方式指定ActivityB的enterTransition:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| <?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<slide
android:duration="1000"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:slideEdge="bottom">
<targets>
<target android:targetId="@id/content_container"/>
</targets>
</slide>
<slide
android:duration="1000"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:slideEdge="top">
<targets>
<target android:targetId="@id/image_container"/>
</targets>
</slide>
</transitionSet>
|
运行效果如下:

上图动画有两个问题:
- ActivityA的exitTransition还没完全走完ActivityB的enterTransition就执行了,ActivityB的returnTransition还没完全走完ActivityA的reenterTransition就执行了;
- 状态栏和导航栏的动画不太协调;
问题1是因为默认情况下enter/return transition会比exit/reenter transition完全结束前稍微快一点运行,如果想让前者完全运行完后者再进来,可以在代码中调用Window的
1
2
| setWindowAllowEnterTransitionOverlap(false)
setWindowAllowReturnTransitionOverlap(false)
|
或者在xml中设置
1
2
| <item name="android:windowAllowEnterTransitionOverlap">false</item>
<item name="android:windowAllowReturnTransitionOverlap">false</item>
|
运行如下:

再看下问题2,默认情况下状态栏和标题栏也会参与动画(如果有导航栏也会,测试机默认木有导航栏),当我们把xxxoverlap属性设为false后就看得比较明显了,如果不想让它们参与动画通过excludeTarget()将其排除,在代码中:
1
2
3
4
5
6
7
8
9
| private void setupTransition() {
Slide slide = new Slide(Gravity.LEFT);
slide.setDuration(1000);
slide.setInterpolator(new FastOutSlowInInterpolator());
slide.excludeTarget(android.R.id.statusBarBackground, true);
slide.excludeTarget(android.R.id.navigationBarBackground, true);
slide.excludeTarget(R.id.appbar,true);
getWindow().setExitTransition(slide);
}
|
或者在xml中:
1
2
3
4
5
6
7
8
9
10
11
| <slide xmlns:android="http://schemas.android.com/apk/res/android"
android:slideEdge="left"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:duration="1000">
<targets>
<!-- if using a custom Toolbar container, specify the ID of the AppBarLayout -->
<target android:excludeId="@id/appbar" />
<target android:excludeId="@android:id/statusBarBackground"/>
<target android:excludeId="@android:id/navigationBarBackground"/>
</targets>
</slide>
|
效果如下:

具体流程
ActivityA startActivity()
- 确定需要执行exit Transition的target View
- Transition的captureStartValues()获取target View Visibility的值(此时为VISIBLE)
- 将target View Visibility的值设为INVISIBLE
- Transition的captureEndValues()获取target View Visibility的值(此时为INVISIBLE)
- Transition的createAnimator()根据前后Visibility的属性值变化创建动画
ActivityB Activity 开始
- 确定需要执行enter Transition的target View
- Transition的captureStartValues()获取获取target View Visibility的,初始化为INVISIBLE
- 将target View Visibility的值设为VISIBLE
- Transition的captureEndValues()获取target View Visibility的值(此时为VISIBLE)
- Transition的createAnimator()根据前后Visibility的属性值变化创建动画
ShareElement Transition
shareElement Transition的例子

shareElement Transition指的是共享元素从activity/fragment到其他activity/fragment时的动画

有了上面的分析看名字应该也猜得出方法对应的功能了,如果没有设置exit/enter shared element transitions,默认为 @android:transition/move,上面的Content Transition是根据Visibility的变化创建动画,而shareElement Transition是根据大小,位置,和外观的变化创建动画,如chanageBounds、changeTransform、ChangeClipBounds、 ChangeImageTransform等,具体API使用和效果可以参考上篇。指定shareElement Transition可以通过代码形式:
1
2
3
4
| getWindow().setSharedElementEnterTransition();
getWindow().setSharedElementExitTransition();
getWindow().setSharedElementReturnTransition();
getWindow().setSharedElementReenterTransition();
|
也可以通过xml形式:
1
2
3
4
5
6
7
8
| <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- specify shared element transitions -->
<item name="android:windowSharedElementEnterTransition">
@transition/change_image_transform</item>
<item name="android:windowSharedElementExitTransition">
@transition/change_image_transform</item>
</style>
|
然后启动Acticity
1
2
3
4
5
6
| Intent intent = new Intent(this, DetailsActivity.class);
// Pass data object in the bundle and populate details activity.
intent.putExtra(DetailsActivity.EXTRA_CONTACT, contact);
ActivityOptionsCompat options = ActivityOptionsCompat.
makeSceneTransitionAnimation(this, (View)ivProfile, "profile");
startActivity(intent, options.toBundle());
|
在布局文件中对于要共享的View添加android:transitionName且保持一致,如果要共享的View有点多,可以通过Pair,Pair<View,String> 存储着共享View和View的名称,使用如下
1
2
3
4
5
6
7
8
| Intent intent = new Intent(context, DetailsActivity.class);
intent.putExtra(DetailsActivity.EXTRA_CONTACT, contact);
Pair<View, String> p1 = Pair.create((View)ivProfile, "profile");
Pair<View, String> p2 = Pair.create(vPalette, "palette");
Pair<View, String> p3 = Pair.create((View)tvName, "text");
ActivityOptionsCompat options = ActivityOptionsCompat.
makeSceneTransitionAnimation(this, p1, p2, p3);
startActivity(intent, options.toBundle());
|
例子
在ActivityB的theme中添加SharedElementEnterTransition
1
2
3
| <item name="android:windowSharedElementEnterTransition">
@transition/change_image_transform
</item>
|
change_image_transform.xml
1
2
3
4
5
6
7
8
9
| <?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds
android:duration="1000"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<changeImageTransform
android:duration="1000"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</transitionSet>
|
执行效果:

具体流程
从图上看,好像图片是从一个ActivityA”传递”到另一个ActivityB,实际上真正负责绘制都发生在ActivityB上:
- ActivityA调用startActivity()后ActivityB处于透明状态
- Transition收集ActivityA中共享View的初识状态,并传递给ActivityB
- Transition收集ActivityB中共享View的最终状态
- Transition根据状态改变创建动画
- Transition隐藏ActivityA,随着ActivityB的共享View运动到指定位置,ActivityB的背景在ActivityA上淡入,并随着动画完成而完全可见。
我们可以通过修改Activity背景淡入淡出时间来验证,在ActivityB中加入
1
| getWindow().setTransitionBackgroundFadeDuration(2000);
|
为了更直观,把ActivityA的exitTransition先注释掉,运行效果:

可以看到,ActivityB确实像盖在ActivityA上,这里用到了 ViewOverlay,原理简单来说就是在其他View上draw,共享View利用该技术可以实现画在其他View上。我们可以通过Window的setSharedElementsUseOverlay(false)来关闭该功能,不过这样一来会使最终结果和你预想的不一致,默认该值为true。
延迟加载
上面分析Transition会获取共享视图前后的状态值来创建动画,如果我们的图片是网上下载的,那么很有可能图片的准确大小需要下载下来才能确定,Activity Transitions API提供了一对方法暂时推迟过渡,直到我们确切地知道共享元素已经被适当的渲染和放置。在onCreate中调用postponeEnterTransition()(API >= 21)或者supportPostponeEnterTransition()(API < 21)延迟过渡;当图片的状态确定后,调用startPostponedEnterTransition()(API >= 21)或supportStartPostponedEnterTransition()(API < 21)恢复过渡,常见处理:
1
2
3
4
5
6
7
8
9
10
11
12
| // ... load remote image with Glide/Picasso here
supportPostponeEnterTransition();
ivBackdrop.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
ivBackdrop.getViewTreeObserver().removeOnPreDrawListener(this);
supportStartPostponedEnterTransition();
return true;
}
}
);
|
Thanks to
Material-Animations
Getting Started with Activity & Fragment Transitions (part 1-4)
What are all these dang Transitions
Shared Element Activity Transition
Define custom animations
Presentation-ContentTransition
作者:HuYounger
本文转载自:http://rkhcy.github.io