一. 简介
Android 4.4.2 中引入了 Transition 过渡动画,不过那时的 API 的功能比较简单,只能对整个 Activity 或 Fragment 做动画,Google 在 Android 5.0 的 Material Design 中引入更完整的 Transition 框架。Android 的过渡动画可以分为五个部分:
组级动画,允许将一个或多个动画效果运用于一个View Hierarchy(以下翻译为视图层级结构)的所有Views;
过渡型动画,根据开始和结束的View的属性值变化执行动画;
内建动画,该框架包含多种常见内建动画效果,例如渐变,移动;
支持资源文件创建,允许通过资源文件加载View和transition animation;
生命周期回调,提供回调方法以更好控制动画和View Hierarchy的变化过程。
相对于View Animation或Property Animator,场景过渡动画更加具有特殊性,可以看作是基于特定业务情景(场景切换)对Property Animator的高度封装。不同于Animator,场景过度动画具有特定的关注点,即如何实现具有视觉连续性的场景切换。
二. 工作过程
Transition 是指不同 UI 状态转换时的动画。其中有两个关键概念:场景(scenes)和转换(transitions)。场景定义了一个确定的 UI 状态,而转换定义了两个场景切换时的动画。
当两个场景切换时,Transition 主要有下面两个行为:
(1)确定开始场景和结束场景中每个 view 的状态。
(2)根据状态差异创建 Animator,用以场景切换时 view 的动画。
上图来自Android 开发官网,transition framework(蓝色部分)与view hierarchy以及animations并行工作。starting scene和ending scene分别保存starting layout 和ending layout的状态,包括它所有的views及views的属性。transition保存了一个或多个属性动画效果。要执行一个transition来实现starting layout过渡到ending layout,需要使用transitionManager,并指定ending scene和需要使用的transition。
三. 应用
1. 不同scene 之间的变换动画
1)创建Scene
一个Scene保存了一个视图层级结构,包括它所有的views以及views的状态。Transition框架可以实现在starting scene和ending scene之间执行动画。大多数情况下,我们不需要创建starting scene,因为starting scene通常由当前UI状态决定,我们只需要创建ending scene。Transition框架允许我们通过XML文件或Java代码创建一个Scene。两种方式都需要使用Scene类。
创建Scene 一般有以下几种方法:
// 这个方法会根据layoutId 来创建一个Scene 对象,如果在sceneRoot 下已经存在了这样一个Scene,会直接返回
public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context)
// 这是一个构造方法,这个方法创建的Scene,在进入时会清空sceneRoot 的所有子View,然后把layout 作为sceneRoot 的新子View 添加进去
public Scene(ViewGroup sceneRoot, View layout)
2)创建Transition
Transiton则是用来设置过渡动画效果用的。而且系统给提供了一些非常使用的Transtion动画效果,如下表所示:
系统Transition | 解释 |
---|---|
ChangeBounds | 检测View的位置边界创建移动和缩放动画(关注布局边界的变化) |
ChangeTransform | 检测View的scale和rotation创建缩放和旋转动画(关注scale和rotation的变化) |
ChangeClipBounds | 检测View的剪切区域的位置边界,和ChangeBounds类似。不过ChangeBounds针对的是view而ChangeClipBounds针对的是view的剪切区域setClipBound(Rect rect) 中的rect(关注的是setClipBounds(Rect rect)rect的变化) |
ChangeImageTransform | 检测ImageView的ScaleType,并创建相应动画(关注的是ImageView的scaleType) |
Fade | 根据View的visibility状态的的不同创建淡入淡动画,调整的是透明度(关注的是View的visibility的状态) |
Slide | 根据View的visibility状态的不同创建滑动动画(关注的是View的visibility的状态) |
Explode | 根据View的visibility状态的的不同创建分解动画(关注的是View的visibility的状态) |
AutoTransition | 默认动画,ChangeBounds、Fade动画的集合 |
创建Transition 可以直接new 一个,也可以在XML 中定义,然后通过TransitionInflater 来inflate 一个,和View 的创建形式类似。
3)使用TransitionManager
TransitionManager 顾名思义,是负责管理Transition 动画的执行,在创建了Scene 和Transition 之后就可以调用TransitionManager 的方法执行动画了。TransitionManager 比较常用的两个方法是:
public static void go(Scene scene)
public static void go(Scene scene, Transition transition)
这两个方法使用给定的transition 效果,变换到指定的scene,如果调用第一个方法不传transition,将使用默认transition。
public static void beginDelayedTransition(final ViewGroup sceneRoot)
public static void beginDelayedTransition(final ViewGroup sceneRoot, Transition transition)
该方法会在调用此方法和下一个渲染帧之间,为给定sceneRoot 中的所有更改定义的新场景设置动画,调用此方法会导致TransitionManager捕获sceneRoot 中的当前值,然后发布请求以在下一帧上运行转换。此时,将捕获sceneRoot 中的新值,并且将对新值和旧值之间的更改进行动画处理。该方法无需创建Scene; 因为在调用此方法和transition 开始时的下一帧之间发生的一系列change 实现了这一点。在下一帧之前多次调用此方法(例如,如果不相关的代码也想要进行动态更改并且在相同sceneRoot 上运行transition),只有第一次调用才会触发捕获值并退出当前场景。在同一帧中对具有相同sceneRoot 的后续方法调用将被忽略。
例:
下面是一个使用ChangeBounds 效果的示例:
class AutoTransitionActivity : AppCompatActivity() {
private var startScene: Scene? = null
private var endScene: Scene? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_auto_transition)
val sceneRoot = findViewById(R.id.scene_root) // 要执行scene 切换的Root ViewGroup
startScene = Scene.getSceneForLayout(sceneRoot, R.layout.layout_scene_start, this) // 创建开始scene,第二个参数是布局文件
endScene = Scene.getSceneForLayout(sceneRoot, R.layout.layout_scene_end, this) // 创建结束scene
TransitionManager.go(startScene) // 先切换到开始scene
findViewById
效果如图
2. Activity、Fragment 之间切换动画
API21之前Activity过渡动画通过两种方式来实现:style主题里面统一设置、或者使用代码overridePendingTransition函数单独设置。(当然了代码设置的优先级要高于style主题里面统一设置)
比如:
在API 21之后google又推出了一种比之前效果更加赞的过渡动画。 通过ActivityOptions + Transition 来实现Activity过渡动画。
启动Activity 里有两个方法,加了Bundle 参数,
public void startActivity(Intent intent, @Nullable Bundle options);
public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options);
options 参数是由ActivityOptions 对象调用toBundle 方法得到的,而生成ActivityOptions 对象有几个静态方法,同时用来设置动画效果:
/**
* 和overridePendingTransition类似,设置跳转时候的进入动画和退出动画
*/
public static ActivityOptions makeCustomAnimation(Context context, int enterResId, int exitResId);
/**
* 通过把要进入的Activity通过放大的效果过渡进去
* 举一个简单的例子来理解source=view,startX=view.getWidth(),startY=view.getHeight(),startWidth=0,startHeight=0
* 表明新的Activity从view的中心从无到有慢慢放大的过程
*/
public static ActivityOptions makeScaleUpAnimation(View source, int startX, int startY, int width, int height);
/**
* 通过放大一个图片过渡到新的Activity
*/
public static ActivityOptions makeThumbnailScaleUpAnimation(View source, Bitmap thumbnail, int startX, int startY);
/**
* 场景动画,体现在两个Activity中的某些view协同去完成过渡动画效果,等下在例子中能更好的看到效果
*/
public static ActivityOptions makeSceneTransitionAnimation(Activity activity, View sharedElement, String sharedElementName);
/**
* 场景动画,同上是对多个View同时起作用
*/
public static ActivityOptions makeSceneTransitionAnimation(Activity activity, android.util.Pair... sharedElements);
makeCustomAnimation、makeScaleUpAnimation、makeThumbnailScaleUpAnimation这三种产生的效果还是走的API 21之前的效果,而且这三种效果好像和Transition动画没啥太多的联系。我们用的最多的还是makeSceneTransitionAnimation()函数,makeSceneTransitionAnimation效果才是和Transition动画效果密切相关的。
Transitionz过渡动画的使用也是有前提的:
API 21以上。当然你也可以不使用ActivityOptions,而是使用兼容类ActivityOptionsCompat来替换ActivityOptions。(兼容类给到我们的作用是保证程序在低版本运行不会挂掉,但是不能保证低版本也能起到响应的效果的)
必须允许Activity可以使用Transition,要么在style里面设置(
- true
),要么直接通过代码设置(getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);)。
对于Transition Activity过渡动画的使用,我们简单的分为三个步骤:
告诉系统以Transition的方式启动Activity、定义过渡动画、设置过渡动画。
-
告诉系统以Transition的方式启动Activity,就是通过带options 参数的startActivity 方法来打开新的Activity
ActivityOptions compat = ActivityOptions.makeSceneTransitionAnimation(mActivity); startActivity(new Intent(mContext, MakeSceneTransitionActivity.class), compat.toBundle());
定义过渡动画,系统给我们提供了三种Transition过渡动画,可以拿来直接使用
系统默认动画 | 解释 |
---|---|
分解(explode) | 从场景中心移入或移出视图 |
滑动(slide) | 从场景边缘移入或移出视图 |
淡入淡出(fade) | 通过调整透明度在场景中增添或移除视图 |
可以有两种方式进行定义动画,一种是通过xml 代码,另一种是Java 代码,
1)xml 代码,在 res/ 目录下创建transition 资源文件夹后,就可以在该文件夹下对每一种动画进行定义,例如
该xml定义了一个slide过渡动画,除了状态栏、导航栏以外其他所有的View进行滑入滑出动画效果。
其中
上面只是定义了一种过渡动画,如果我们想要在同一个过渡状态中实现两种或多种动画效果怎么办?也简单,将根标签替换为
该xml定义了两个过渡动画slide、fade。顺序执行。slide 动画针对 id 为 image_shared 的 view 进行下面滑入,fade 动画将除了状态栏、导航栏和 id 为image_shared 以外的 view,进行淡入淡出。
2)使用Java 代码,可以用资源文件来定义的对象,那咱们就一定可以用代码的方式来实现,我们用代码来实现上述多种动画效果对应的资源文件:
// //资源文件指定过渡动画
// getWindow().setEnterTransition(TransitionInflater.from(this).inflateTransition(R.transition.transition_target));
//代码制定过渡动画
TransitionSet transitionSet = new TransitionSet();
//slide动画
Slide slide = new Slide(Gravity.BOTTOM);
slide.addTarget(R.id.image_shared);
transitionSet.addTransition(slide);
//fade动画
Fade fade = new Fade();
fade.excludeTarget(android.R.id.statusBarBackground, true);
fade.excludeTarget(android.R.id.navigationBarBackground, true);
fade.excludeTarget(R.id.image_shared, true);
transitionSet.addTransition(fade);
transitionSet.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
getWindow().setEnterTransition(transitionSet);
- 设置动画
告诉Activity以Transition的方式启动了,也定义好了过渡动画了。接下来就是去设置过渡动画了。Transition过渡动画的设置可以在style文件中统一设置也可以在代码中设置(代码中设置的优先级比style主题文件优先级高)。
Java 代码 | theme 设置 | 作用 |
---|---|---|
getWindow().setEnterTransition() | android:windowEnterTransition | A启动B,B中的View进入场景的transition(代码所在位置B) |
getWindow().setExitTransition() | android:windowExitTransition | A启动B,A中的View退出场景的transition(代码所在位置A) |
getWindow().setReturnTransition() | android:windowReturnTransition | B返回A,B中的View退出场景的transition(代码所在位置B) |
getWindow().setReenterTransition() | android:windowReenterTransition | B返回A,A中的View重新进入场景的transition(代码所在位置A) |
Activity过渡动画使用的时候有一个设置可以提高展示效果,可以通过在主题中设置windowAllowEnterTransitionOverlap、windowAllowReturnTransitionOverlap让动画过渡的更加自然。
其中windowAllowEnterTransitionOverlap表示进入动画是否可以覆盖别的动画、windowAllowReturnTransitionOverlap表示返回动画是否可以覆盖别的动画。
3. Activity 切换时共享元素
当你想要从一个Activity A转换到Activity B,而且他们共享一个元素(比如是一个view),在这种场景下,最好的用户体验可能就是将共享的元素直接变换到最终的地方和大小,这会使用户专注于应用而且有一种连贯性的表达。
共享元素的连接点是所有共享元素View的transition name。它可以在layout文件里面设置(android:transitionName)、也可以代码设置(View.setTransitionName("name");),通过transtion name来判断哪两个元素是共享关系。
有了前面Activity过渡动的理解,共享元素动画在理解上就简单的多了。同Activity过渡动画一样,共享元素的动画也可以通过代码或者主题文件来设置(Fragment里面共享元素动画的设置可以类比Activity里面共享元素动画的设置),如下所示。
Java 代码 | theme 设置 | 释义 |
---|---|---|
getWindow().setSharedElementEnterTransition() | android:windowSharedElementEnterTransition | A启动B,B中的View共享元素的transition(代码所在位置B) |
getWindow().setSharedElementExitTransition() | android:windowSharedElementExitTransition | A启动B,A中的View共享元素transition(代码所在位置A) |
getWindow().setSharedElementReturnTransition() | android:windowSharedElementReturnTransition | B返回A,B中的View共享元素的transition(代码所在位置B) |
getWindow().setSharedElementReenterTransition() | android:windowSharedElementReenterTransition | B返回A,A中的View重新进入共享元素的transition(代码所在位置A) |
同样和Activity过渡动画一样的也可以给Transiton设置回调监听,比如监听Transition开始和结束等等。
3.1 更新元素对应关系
有这种情况,比如我们第一个界面是一个列表(RecyclerView)每个item都是一个图片,点击进入另一个页面详情页面,详情页面呢有是ViewPager的形式。可以左右滑动。咱们有的时候就想,就算详情界面滑动到了其他照片,在返回到第一个页面的时候也想要有共享元素动画的效果。这个时候就得更新下共享元素的对应关系了。
怎么更新呢,关键是看SharedElementCallback 类的onMapSharedElements()函数,这个函数是用来装载共享元素的。
比如有这么个情况,还是上面的例子A界面跳转到B界面。那么A界面在B返回的时候要更新下、B界面在返回之前要更新下。所以给A界面设置setExitSharedElementCallback(SharedElementCallback);、给B界面设置setEnterSharedElementCallback(SharedElementCallback)。
注:
setExitSharedElementCallback(SharedElementCallback)的SharedElementCallback 里面的onMapSharedElements() 函数在Activity exit 和reenter 时都会触发
setEnterSharedElementCallback(SharedElementCallback)的SharedElementCallback 里面的onMapSharedElements() 函数在Activity enter 和return 时都会触发。
3.2 延时共享元素动画
有的时候又有这种情况,比如要展示一个网络图片,在网络图片获取到之前,这个共享元素的动画效果没啥作用。需要等图片获取完成之后在开始共享元素的动画效果,这个时候延时元素动画就派上用场了。
postponeEnterTransition() 函数用于延时动画,
startPostponedEnterTransition() 函数用于开始延时的共享动画。
那我们就可以,在Activity进入的时候先调用postponeEnterTransition() 延时动画,在网络图片获取完成之后在调用startPostponedEnterTransition() 开始动画。