Transition 概念 Transition 是指不同 UI 状态转换时的动画。其中有两个关键概念:场景(Scene)和转换(Transition)。场景定义了一个确定的 UI 布局状态,而转换定义了两个场景切换时过渡的动画。
当两个场景进行切换时,Transition 主要有下面两个行为:
1、 确定开始场景和结束场景中每个 view 的状态。 2、 根据状态差异创建 Animator,用于场景切换时每个 view 的动画。
例如最简单的对View的隐藏增加渐变动画:
1
2
TransitionManager.beginDelayedTransition(viewGroup, new Fade());
view.setVisibility(View.GONE);
动画执行的基本流程: 1、TransitionManager.beginDelayedTransition(viewGroup, new Fade()); 确定子view初始状态。 2、调用view.setVisibility(View.GONE);之后,framework会调用Transition类的captureEndValues()方法,记录每个view最新的可见状态。 3、 framework调用Transition的createAnimator()方法。transition会分析每个view的开始和结束时的数据发现view在开始时是可见的,结束时是不可见的。Fade(Transition的子类)会利用这些信息创建一个用于把view的alpha属性变为0的 Animator,并返回 Animator 对象。 4、framework会执行返回的 Animator 动画
beginDelayedTransition 方法分析 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void beginDelayedTransition (final ViewGroup sceneRoot, Transition transition) {
... ...
sceneChangeSetup(sceneRoot, transitionClone);
Scene.setCurrentScene(sceneRoot, null );
sceneChangeRunTransition(sceneRoot, transitionClone);
}
private static void sceneChangeRunTransition (final ViewGroup sceneRoot, final Transition transition) {
MultiListener listener = new MultiListener(transition, sceneRoot);
sceneRoot.addOnAttachStateChangeListener(listener);
sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
}
private static class MultiListener implements ViewTreeObserver .OnPreDrawListener , View .OnAttachStateChangeListener {
... ...
@Override
public boolean onPreDraw () {
... ...
mTransition.captureValues(mSceneRoot, false );
if (previousRunningTransitions != null ) {
for (Transition runningTransition : previousRunningTransitions) {
runningTransition.resume(mSceneRoot);
}
}
mTransition.playTransition(mSceneRoot);
return true ;
}
};
页面过渡动画 过渡动画使得 Activity 跳转或者 Fragment 切换等显得不那么生硬,通过共享元素(Share Element)过渡决定了两个 Activity/Fragment 共享的视图如何在这些跳转的时候执行过渡动画。例如,如果两个 Activity 使用相同的图片(但位置和大小不同),通过 changeImageTransform 共享元素过渡就会在这些 Activity/Fragment 之间流畅地平移和缩放该图片。如图所示共享元素过渡效果:
支持的Android最低版本:Android 5.0 (API 21)
Content Transition 内容变换(Content Transition)决定了非共享view元素 在 activity 和 fragment 切换期间是如何进入或者退出场景的。Content Transition 的触发是通过改变 view 的 visibility 来实现的。
setExitTransition() - 当A start B时,使A中的View退出场景的transition
setEnterTransition() - 当A start B时,使B中的View进入场景的transition
setReturnTransition() - 当B 返回 A时,使B中的View退出场景的transition
setReenterTransition() - 当B 返回 A时,使A中的View进入场景的transition
Shared Element Transition 共享元素变换(Shared Element Transition)决定了共享view元素 从一个 Activity/Fragment 到另一个 Activity/Fragment t 的切换中是如何动画变化的。共享元素变换并不是真正实现了两个activity或者Fragment之间元素的共享,Framework采用了不同的方法来达到相同的视觉效果。共享元素默认其实是绘制在整个view树结构的最上层,在一个叫ViewOverlay的东西上面。
setSharedElementEnterTransition() - 设置在A进入B的时候播放的动画,共享元素以A中的位置作为起始,B中的位置为结束来播放动画。
setSharedElementReturnTransition() - 设置在B返回A的时候播放的动画,共享元素以B中的位置作为起始,A中的位置为结束来播放动画。
Activity 共享元素过渡动画 设置允许过渡动画 按官方文档的说法,使用过渡动画之前需要在 Activity 设置启用,须在 setContentView() 之前调用,最好是放在 super.onCreate() 之前,避免 6.0 及以下机型报错:requestFeature() must be called before adding content。
1
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
或者在Theme的定义里面加上
1
<item name ="android:windowActivityTransitions" > true</item >
开启过渡动画 例如上面示例图里面,由搜索结果页跳商详页,主图缩放效果
1、两个页面执行过渡的共享元素分别设置相同的 transitionName1
2
3
<ImageView
...
android:transitionName ="imgTransition" />
2、执行过渡动画跳转1
2
3
4
5
6
7
8
9
10
11
12
13
startActivity(new Intent(this , DetailActivity.class), ActivityOptions.makeSceneTransitionAnimation(this ).toBundle());
startActivity(new Intent(this , DetailActivity.class),
ActivityOptionsCompat.makeSceneTransitionAnimation(this , imageView, "imgTransition" ).toBundle());
Pair<View, String> pair1 = Pair.create((View) image, "imgTransition" );
Pair<View, String> pair2 = Pair.create((View) title, "titleTransition" );
...
ActivityOptionsCompat transitionActivityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(ActTransitionActivity.this , pair1, pair2, ...);
startActivity(new Intent(this , DetailActivity.class), transitionActivityOptions.toBundle());
3、由于一个页面布局里面不允许有重名的 transitionName 的元素,对于列表,例如 RecyclerView,可以在 onBindViewHolder 的时候,手动设置 transitionName。1
2
3
4
public void onBindViewHolder (final ViewHolder viewHolder, final int position) {
...
ViewCompat.setTransitionName(viewHolder.image, position + "_imgTransition" );
}
那么点击跳转到商详页的时候,把对应位置的 transitionName 通过参数带过去,并设置给商详页主图 ImageView 的 transitionName。
Tips 1、页面退出时,应该调用 Activity.finishAfterTransition() ,不能直接调用 finish(), 否则过渡动画将不执行。
2、过渡动画过程包括 Shared Element Transition
和 Content Transition
, 指共享元素 和 其他非共享元素 的过渡动画。
Fragment 共享元素过渡动画 Fragment 的过渡动画是天然支持的,在为 Fragment 添加 Transition 的时候并不需要像 Activity 一样设置 Window.FEATURE_ACTIVITY_TRANSITIONS 和 Window.FEATURE_CONTENT_TRANSITIONS。
例如,FragmentA 切换到 FragmentB,需要分别设置共享元素的 transitionName。Fragment 同样也支持设置过渡动画类型
1
2
3
4
5
6
Fragment.setSharedElementEnterTransition()
Fragment.setSharedElementExitTransition()
Fragment.setExitTransition()
Fragment.setEnterTransition()
Fragment.setReturnTransition()
Fragment.setReenterTransition()
1. 对于fragment通过 replace() 的情况:
1
2
3
4
5
6
getSupportFragmentManager()
.beginTransaction()
.addSharedElement(holder.image, "imgTransition" )
.replace(R.id.container, FragmentB)
.addToBackStack(null )
.commit();
2. 对于fragment通过 add()/show()/hide() 的情况
1
2
3
4
5
6
7
8
getSupportFragmentManager()
.beginTransaction()
.addSharedElement(holder.image, "imgTransition" )
.add(R.id.container, FragmentB)
.show(FragmentB)
.hide(FragmentA)
.setReorderingAllowed(true )
.commit();
过渡动画分析 页面进入和退出时,过渡动画执行顺序如图所示:
动画类型 过渡时,可以设置一些动画效果(android.transition.Transition
),一般默认是 Fade(淡入淡出)或 AutoTransition。系统还提供了其它一些动画,基本适用于大部分场景了。
Slide(滑动式,可以选择滑动方向)
Explode(分解,类似Slide,但是会根据共享元素中心做方向计算,可以理解为周围发散)
ChangeBounds(布局边界的变化 动画效果)
ChangeClipBounds(裁剪边界的变化 动画效果)
ChangeTransform(缩放和旋转方面的变化 动画效果)
ChangeImageTransform (尺寸和缩放方面的变化 动画效果)
AutoTransition(实际上包含了Fade-out,ChangeBounds和Fade-in的集合,对共享元素设置时包含的Fade无效果)
TransitionSet(实现动画集合,AutoTransition 基于此类实现)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
getWindow().setExitTransition(new Slide());
getWindow().setReenterTransition(null );
getWindow().setEnterTransition(new Explode());
getWindow().setReturnTransition(new Fade());
getWindow().setSharedElementExitTransition(new Fade().setDuration(1000 ));
getWindow().setSharedElementEnterTransition(new AutoTransition());
也可以分别在两个 Activity 的 Theme 里面配置转场动画效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<style name ="BaseAppTheme" parent ="android:Theme.Material" >
<item name ="android:windowActivityTransitions" > true</item >
<item name ="android:windowEnterTransition" > @transition/explode</item >
<item name ="android:windowExitTransition" > @transition/explode</item >
<item name ="android:windowSharedElementEnterTransition" >
@transition/change_image_transform</item >
<item name ="android:windowSharedElementExitTransition" >
@transition/change_image_transform</item >
</style >
1
2
3
4
<transitionSet xmlns:android ="http://schemas.android.com/apk/res/android" >
<changeImageTransform />
</transitionSet >
对于上述的各种 Transition,我们也可以设置其动画时长、插值器等。
1
2
3
Transition.setDuration(300 );
Transition.setInterpolator(new FastOutSlowInInterpolator());
Transition.setStartDelay(200 );
自定义 Transition 若系统提供的动画无法满足需求,也可以扩展 Visibility/Transition 类实现自定义转场动画效果。例如自定义一个文字大小和颜色渐变的 Transition。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public class ChangeTextTransition extends Transition {
protected static final String PROPNAME_TEXTSIZE = "ChangeTextTransition::textSize" ;
protected static final String PROPNAME_TEXTCOLOR = "ChangeTextTransition::textColor" ;
public ChangeTextTransition () {
addTarget(TextView.class);
}
@Override
public void captureStartValues (TransitionValues transitionValues) {
ShareElementInfo info = ShareElementInfo.getFromView(transitionValues.view);
if (info == null || !(info.getViewStateSaver() instanceof TextViewStateSaver)) {
return ;
}
captureValues(transitionValues, (TextViewStateSaver) info.getViewStateSaver(), info.isEnter() ? info.getFromViewBundle() : info.getToViewBundle());
}
@Override
public void captureEndValues (TransitionValues transitionValues) {
ShareElementInfo info = ShareElementInfo.getFromView(transitionValues.view);
if (info == null || !(info.getViewStateSaver() instanceof TextViewStateSaver)) {
return ;
}
captureValues(transitionValues, (TextViewStateSaver) info.getViewStateSaver(), info.isEnter() ? info.getToViewBundle() : info.getFromViewBundle());
}
protected void captureValues (TransitionValues value, TextViewStateSaver stateSaver, Bundle viewExtraInfo) {
value.values.put(PROPNAME_TEXTSIZE, stateSaver.getTextSize(viewExtraInfo));
value.values.put(PROPNAME_TEXTCOLOR, stateSaver.getTextColor(viewExtraInfo));
}
@Override
public Animator createAnimator (ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
ShareElementInfo info = endValues==null ?null :ShareElementInfo.getFromView(endValues.view);
if (info == null || !(info.getViewStateSaver() instanceof TextViewStateSaver)) {
return null ;
}
final TextView view = (TextView) endValues.view;
view.setPivotX(0f );
view.setPivotY(0f );
float startTextSize = (float ) startValues.values.get(PROPNAME_TEXTSIZE);
final float endTextSize = (float ) endValues.values.get(PROPNAME_TEXTSIZE);
ObjectAnimator textSizeAnimator = ObjectAnimator.ofFloat(view, new TextSizeProperty(), startTextSize, endTextSize);
int startTextColor = (int ) startValues.values.get(PROPNAME_TEXTCOLOR);
int endTextColor = (int ) endValues.values.get(PROPNAME_TEXTCOLOR);
ObjectAnimator textColorAnimator = ObjectAnimator.ofArgb(view, new TextColorProperty(), startTextColor, endTextColor);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(textSizeAnimator, textColorAnimator);
return animatorSet;
}
private class TextSizeProperty extends Property <TextView , Float > {
public TextSizeProperty () {
super (Float.class, "textSize" );
}
@Override
public void set (TextView object, Float value) {
object.setTextSize(TypedValue.COMPLEX_UNIT_PX, value);
}
@Override
public Float get (TextView object) {
return object.getTextSize();
}
}
private class TextColorProperty extends Property <TextView , Integer > {
public TextColorProperty () {
super (Integer.class, "textColor" );
}
@Override
public void set (TextView object, Integer value) {
object.setTextColor(value);
}
@Override
public Integer get (TextView object) {
return object.getCurrentTextColor();
}
}
}
Transition Overlap 默认情况下,内容过渡动画的后一个页面的 Enter/Return 转换会在 前一个页面的 Exit/Reenter 转换结束前一点开始,产生一个小的重叠来让整体的效果更自然、更协调。默认 overlap 是 true,进入转换会退出转换开始后尽可能快地开始,如果设置为 false,进入转换只能在退出转换结束后开始,通常都为 true。
1
2
getWindow().setAllowEnterTransitionOverlap(true );
getWindow().setAllowReturnTransitionOverlap(true );
Shared Element Overlay 默认情况下,共享元素视图是绘制在整个视图结构之上的(的 ViewOverlay 层)。但是如果共享元素周围有点击效果,例如 ?attr/selectableItemBackground
波纹效果,那么如果共享元素视图绘制在整个视图结构之上,点击时在波纹效果还没消失的时候,会堆叠一个共享元素视图,比较影响协调性,可以把 sharedElementsUseOverlay 置为 false。
1
getWindow().setSharedElementsUseOverlay(false )
更新共享元素
如上图所示,共享元素过渡跳转到预览页后,我们可以通过 ViewPager 切换到其他元素,那么返回时如果未更新共享元素对应关系,则返回时会出现找不到对应的共享元素,而无法执行过渡动画。理想情况如下图所示:
那么我们可以通过 SharedElementCallback 来更新共享元素对应关系。
对于列表页 ListActivity:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
setExitSharedElementCallback(new SharedElementCallback() {
private void removeOldViews (List<String> names, Map<String, View> sharedElements) {
if (!names.isEmpty()) {
for (String name : names) {
sharedElements.remove(name);
}
names.removeAll(namesTobeRemoved);
}
}
@Override
public void onMapSharedElements (List<String> names, Map<String, View> sharedElements) {
removeOldViews(names, sharedElements);
ViewHolder viewHolder = recyclerView.findViewHolderForPosition(currentPosition);
View imageView = viewHolder.itemView.findViewById(R.id.image);
names.add(imageView.getTransitionName());
sharedElements.put(imageView.getTransitionName(), imageView);
}
});
对于预览页 PreviewActivity:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ActivityCompat.setExitSharedElementCallback(this , new SharedElementCallback() {
private void removeOldViews (List<String> names, Map<String, View> sharedElements) {
if (!names.isEmpty()) {
for (String name : names) {
sharedElements.remove(name);
}
names.removeAll(namesTobeRemoved);
}
}
@Override
public void onMapSharedElements (List<String> names, Map<String, View> sharedElements) {
removeOldViews(names, sharedElements);
Fragment fragment = getCurrentFragment();
if (fragment != null ) {
View view = fragment.getView();
ImageView imageView = view.findViewById(R.id.image);
sharedElements.put(imageView.getTransitionName(), imageView);
names.add(imageView.getTransitionName());
}
}
});
Tips 1、在PreviewActivity ViewPager 切换时,需要把当前的 position 通知给 ListActivity,RecyclerView 自动滚动到对应 position 位置,返回时才能获取到对应的 ViewHolder 2、若不希望实时更新 position,也可以在预览页返回的时候的时候再更新,那么可能就需要延迟过渡动画,等待 RecyclerView 滚动到对应位置绘制完成,再执行过渡动画。对于 Activity 可以通过 postponeEnterTransition()
和 startPostponedEnterTransition()
来停止和恢复过渡动画,对于 Fragment 可以通过 getActivity().supportPostponeEnterTransition()
和 getActivity().supportStartPostponedEnterTransition()
来停止和恢复过渡动画
布局变化过渡动画 布局变化自动监听 1
TransitionManager.beginDelayedTransition(viewGroup)
当我们调用 TransitionManager.beginDelayedTransition(viewGroup)
方法时,会立即记住当前 viewGroup 底下子节点的状态,然后在下一帧中再次记录 viewGroup 所有子节点的状态,根据状态差异执行过渡动画。默认动画是 AutoTransition。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// CHANGE SIZE
TransitionManager.beginDelayedTransition(rootViewGroup);
ViewGroup.LayoutParams layoutParams = imageView.getLayoutParams();
if (sizeChanged) {
layoutParams.width = 200;
layoutParams.height = 200;
} else {
layoutParams.width = 100;
layoutParams.height = 100;
}
imageView.setLayoutParams(layoutParams);
// CHANGE POSITION
TransitionManager.beginDelayedTransition(rootViewGroup, new AutoTransition());
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) imageView.getLayoutParams();
if (locationChanged) {
layoutParams.gravity = Gravity.CENTER;
} else {
layoutParams.gravity = Gravity.LEFT;
}
imageView.setLayoutParams(layoutParams);
animateLayoutChanges 属性 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android ="http://schemas.android.com/apk/res/android"
...
android:animateLayoutChanges ="true" >
<ImageView
android:layout_width ="48dp"
android:layout_height ="48dp"
android:src ="@mipmap/ic_launcher" />
<TextView
android:id ="@+id/tvText"
android:layout_width ="wrap_content"
android:layout_height ="wrap_content"
android:text ="some text" />
</LinearLayout >
通过布局属性 animateLayoutChanges,可以为子 View 变化自动执行过渡动画。如图所示:
场景过渡框架 前文主要说明 Activity 之间跳转的过渡动画,那么如果在同一个 Activity 内的各个组件之间打造过渡动画效果,就可以通过场景过渡框架。场景过渡有两个关键概念:场景(Scene)和转换(Transition),简单的说就是每个 Scene 提供对应的UI布局组件,Transition 负责 Scene 执行两个 Scene 之间变换的过渡动画执行。对于两个场景之间添加过渡动画效果流程如下:
1、为起始布局和结束布局分别创建一个 Scene 对象。 2、创建一个 Transition 对象以定义所需的动画类型。 3、调用 TransitionManager.go(),然后系统会运行动画以交换布局。
布局变化场景动画 为两个布局创建Scene。
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android ="http://schemas.android.com/apk/res/android"
android:id ="@+id/scene_root"
android:layout_width ="match_parent"
android:layout_height ="match_parent" >
<include layout ="@layout/a_scene" />
</FrameLayout >
第一个场景布局:res/layout/a_scene.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android"
android:id ="@+id/scene_container"
android:layout_width ="match_parent"
android:layout_height ="match_parent"
android:orientation ="horizontal" >
<TextView
android:id ="@+id/text_view1"
android:layout_width ="wrap_content"
android:layout_height ="wrap_content"
android:text ="Text Line 1" />
<TextView
android:id ="@+id/text_view2"
android:layout_width ="wrap_content"
android:layout_height ="wrap_content"
android:text ="Text Line 2" />
</LinearLayout >
第二个场景布局:res/layout/b_scene.xml,更改了布局方向和文本排列方式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"?>
<LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android"
android:id ="@+id/scene_container"
android:layout_width ="match_parent"
android:layout_height ="match_parent"
android:orientation ="vertical" >
<TextView
android:id ="@+id/text_view2"
android:layout_width ="wrap_content"
android:layout_height ="wrap_content"
android:text ="Text Line 2" />
<TextView
android:id ="@+id/text_view1"
android:layout_width ="wrap_content"
android:layout_height ="wrap_content"
android:text ="Text Line 1" />
</LinearLayout >
注意:两个场景布局需要做过渡动画的节点id需要保持一致!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SceneTransitionActivity extends AppCompatActivity {
@Override
protected void onCreate (@Nullable Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
setContentView(R.layout.activity_scene_transition);
ViewGroup sceneRoot = (ViewGroup) findViewById(R.id.scene_root);
Scene aScene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this );
final Scene bScene = Scene.getSceneForLayout(sceneRoot, R.layout.b_scene, this );
sceneRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick (View v) {
TransitionManager.go(bScene, new ChangeBounds());
}
});
}
}
执行场景动画效果如图所示: