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、两个页面执行过渡的共享元素分别设置相同的 transitionName
1 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.xml
1 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 ()); } }); } }
执行场景动画效果如图所示: