Android 过渡动画 浅析

什么是过渡动画

过渡动画是Android 5.0 以后提供同的一套动画框架,那么什么是过渡动画呢?借助 官网的解释

使用 过渡布局变化 添加 动画效果

这个解释其实非常的准确,想一个使用场景,Activity/Fragment 切换到其他界面的时候,是否也是一样,布局发生了变化,只需要添加对应的效果,我们也可以做出,界面切换时候炫酷动画效果。当然界面切换只是使用场景之一。

小误区

这里需要说明一下,网上有很多文章提到一个概念:场景动画。在 官网 使用动画启动 Activity 中,也有提到 场景过渡 所谓的场景动画,无外乎也是 布局变化。不要被太多的概念性名词,混淆了学习的脚步。

过渡动画有哪些种类

放上全家福给大家看一下

每一种大致的介绍也放在下面

方法说明
TransitionSet组合效果
AutoTransition默认过渡动画,Fade out 渐隐, move 位移 和 resize 大小缩放,fade in 渐显 ,按顺序
ChangeBounds检测view的位置边界创建移动和缩放动画
ChangeClipBounds检测view的剪切区域的位置边界,和ChangeBounds类似。不过ChangeBounds针对的是view而ChangeClipBounds针对的是view的剪切区域(setClipBound(Rect rect) 中的rect)。如果没有设置则没有动画效果
ChangeImageTransform检测ImageView(这里是专指ImageView)的尺寸,位置以及ScaleType,并创建相应动画 一般时候,我们都是和 ChangeBounds 一起使用,能够做出很漂亮的动画
ChangeTransform检测view的scale和rotation创建缩放和旋转动画
ChangeScroll改变滑动位置
Explode分解效果
Fadeandroid:fadingMode 淡入淡出 有 fade_in,fade_out ,fade_in_out
Slideandroid:slideEdge 从哪边滑动出,有 left, top, right, bottom, start, end 模式

如何使用过渡动画

看一下官网如何说的

  1. 为起始布局和结束布局创建一个 Scene 对象。然而,起始布局的场景通常是根据当前布局自动确定的。
  2. 创建一个 Transition 对象以定义所需的动画类型。
  3. 调用 TransitionManager.go(),然后系统会运行动画以交换布局。

并且还贴心的加上了一个图

下面以一个简单的示例作为例子

第一步 创建Scene

首先是创建两个不同的 场景 scene_changebounds_1 和 scene_changebounds_2 布局如下

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
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/tv_one"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="200dp"
android:background="#FF6200EE"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<View
android:id="@+id/tv_two"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="20dp"
android:background="#FFBB86FC"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_one" />


</androidx.constraintlayout.widget.ConstraintLayout>
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
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/tv_two"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="200dp"
android:background="#FFBB86FC"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<View
android:id="@+id/tv_one"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="20dp"
android:background="#FF6200EE"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_two" />


</androidx.constraintlayout.widget.ConstraintLayout>

将布局添加到 父类的容器当中 父类的容器布局如下

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">


<FrameLayout
android:id="@+id/scene_parent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />


<com.google.android.material.button.MaterialButton
android:id="@+id/btn_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:layout_marginEnd="3dp"
android:background="#FF6200EE"
android:text="转换"
android:textAllCaps="false"
android:textColor="#ffffff"
android:textSize="15sp" />



</LinearLayout>

然后就是真的创建 Scene

1
2
3
4
5
6
7
private fun createSceneChangeBounds1(): Scene {
return Scene.getSceneForLayout(scene_parent, R.layout.scene_changebounds_1, this)
}

private fun createSceneChangeBounds2(): Scene {
return Scene.getSceneForLayout(scene_parent, R.layout.scene_changebounds_2, this)
}

最后使用 TransitionManager.go 方法 切换场景

1
TransitionManager.go(scene2, ChangeBounds())

看一下最终的效果

其他效果就不一一展示。

TransitionManager 分析

接下来我们分析一下 TransitionManager 所有的方法 一张图告诉你

图片截取自 TransitionManager

使用场景动画做转场动画

在上面分析 我们知道,在5.0 极其以上的版本, 过渡动画 其实也可以放在 Activity/Fragment 转场时候。

在做这个之前 我们需要搞懂一个概念,界面的界面其实是可以分成4个动画

对应的 方法

  • Window.setEnterTransition()
  • Window.setExitTransition()
  • Window.setReenterTransition()
  • Window.setReturnTransition()

下面就开始我们的动画效果

检查系统版本

为了兼容,5.0之前的使用方式会放在以后进行说明, 在官网 中,按照他的不走一步一步来呗

1
2
3
4
5
6
// Check if we're running on Android 5.0 or higher
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Apply activity transition
} else {
// Swap without transition
}

启用窗口内容过渡

两种配置方式

style 样式中配置

1
2
3
4
<style name="BaseAppTheme" parent="android:Theme.Material">
<!-- enable window content transitions -->
<item name="android:windowActivityTransitions">true</item>
</style>

代码中配置

1
2
3
4
5
6
7
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//启用窗口内容过渡 要在 setContentView 之前设置
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
setContentView(R.layout.activity_first)

}

指定自定义过渡

同样的,指定自定义过渡 也有两种方式

xml 方式

res/transition 下 创建过渡动画的资源

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:slideEdge="left"
android:interpolator="@android:interpolator/accelerate_decelerate" />

然后使用 inflateTransition 方法 加载 资源

1
2
3
4
5
val transition = TransitionInflater.from(this)
.inflateTransition(R.transition.transition_slide)
window.exitTransition = transition
val compat = ActivityOptionsCompat.makeSceneTransitionAnimation(this)
ActivityCompat.startActivity(this, intent, compat.toBundle())

除了自定义 系统也为我们提供了一些资源,在 android.R.transition 下有一些常用的过渡效果

1
2
val transition =
TransitionInflater.from(this).inflateTransition(android.R.transition.slide)

代码方式

和xml 不同的是获取资源的部分替换了

1
2
3
4
5
6
7
val intent = Intent(this, SecondAct::class.java)
window.exitTransition = Slide().apply {
duration = 1000
slideEdge = Gravity.LEFT
}
val compat = ActivityOptionsCompat.makeSceneTransitionAnimation(this)
ActivityCompat.startActivity(this, intent, compat.toBundle())

添加返回的动画

在SecondAct 中 设置 enterTransition 的动画

1
2
3
4
window.enterTransition = Slide().apply {
duration = 1000
slideEdge = Gravity.RIGHT
}

并且使用

1
ActivityCompat.finishAfterTransition(this)

替换 finish() 方法

最终实现效果如下

🔥 特别注意

如果没有指定returnTransitionreenterTransition,返回 FirstActivity 时会分别执行 反转 的进入和退出转换

ActivityOptionsCompat 浅析

先看一下他所有的方法

方法说明
makeCustomAnimation自定义动画,传入 res/anim 下的资源id
makeScaleUpAnimation某个固定的坐标以某个大小扩大至全屏
makeThumbnailScaleUpAnimation将一个设置的缩略图 缩放到正在启动的新的Activity
makeClipRevealAnimation新的Activity从屏幕的一小块原始区域显示到其最终的完整表示
makeSceneTransitionAnimation共享元素动画

makeCustomAnimation

1
2
3
public static ActivityOptions makeCustomAnimation (Context context, 
int enterResId,
int exitResId)

自定义动画,将动画资源传入,从而实现动画效果

资源文件 放在 res/anim

  • enterResId 进场动画 (B Activity 出现的动画)传0 表示没有动画
  • exitResId 退场动画 (A Activity 消失的动画)传0 表示没有动画

先看一下效果

简单的示例

创建 enter_in 和 enter_out 动画

  • enter_in
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="100%"
android:toXDelta="0"
android:duration="1000">
</translate>
  • enter_out
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="0"
android:toXDelta="-100%"
android:duration="1000">
</translate>
  • 代码中应用
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
//方式1 
val intent = Intent(this, SecondAct::class.java)
//参数1 enterResId 进场动画 (B Activity 出现的动画)
//参数2 exitResId 退场动画 (A Activity 消失的动画)
val compat =
ActivityOptions.makeCustomAnimation(
this,
R.anim.enter_in,
R.anim.enter_out
)
startActivity(intent, compat.toBundle())


//方式2
val compat = ActivityOptionsCompat.makeCustomAnimation(
this,
R.anim.enter_in,
R.anim.enter_out
)
ActivityCompat.startActivity(this, intent, compat.toBundle())


//方式3
val compat = ActivityOptionsCompat.makeCustomAnimation(
this,
R.anim.enter_in,
R.anim.enter_out
)
startActivity(intent, compat.toBundle())

ActivityOptions 和 ActivityOptionsCompat 区别

ActivityOptionsCompat && ActivityOptions

ActivityOptions 是帮助我们处理 转场动画的,

ActivityOptionsCompat则是向后兼容的方式访问ActivityOptions中的特性

小提示

看一下 传入的方法 enterResId exitResId 对比之下能够知道,这个方法只能设置 A—->B 的动画,不支持 B 返回 A 的动画

makeScaleUpAnimation

某个固定的坐标以某个大小扩大至全屏

1
2
public static ActivityOptions makeScaleUpAnimation(View source,
int startX, int startY, int width, int height)
  • 参数1 source 参照物 指定从哪个View的坐标开始放大
  • 参数2 startX 指定以View的X坐标为放大中心的X坐标
  • 参数3 startY 指定以View的Y坐标为放大中心的Y坐标
  • 参数4 width 指定放大前新Activity是多宽 0 表示 从无到有
  • 参数5 height 指定放大前新Activity是多高 0 表示 从无到有

下面的代码是 以 图片中心点为起点,开始进行过度动画,当返回FirstAct 的时候,依然有动画效果

1
2
3
4
5
6
7
8
9
val intent = Intent(this, SecondAct::class.java)
val compat = ActivityOptions.makeScaleUpAnimation(
image,
image.width / 2,
image.height / 2,
0,
0
)
ActivityCompat.startActivity(this, intent, compat.toBundle())

makeThumbnailScaleUpAnimation

将一个设置的缩略图 缩放到正在启动的新的Activity

1
2
public static ActivityOptionsCompat makeThumbnailScaleUpAnimation(@NonNull View source,
@NonNull Bitmap thumbnail, int startX, int startY)
  • 参数1 source 传入View 用来确定第二个界面的坐标,以View 左上角作为中心点
  • 参数1 thumbnail 缩略图
  • 参数1 startX 相对于参数1 的中心点坐标 x 轴的距离
  • 参数1 startY 相对于参数1 的中心点坐标 y 轴的距离
1
2
3
4
5
6
7
8
val intent = Intent(this, SecondAct::class.java)
val compat = ActivityOptionsCompat.makeThumbnailScaleUpAnimation(
image,
BitmapFactory.decodeResource(resources, R.drawable.nm),
0,
0
)
ActivityCompat.startActivity(this, intent, compat.toBundle())

效果需要拿出8倍镜才能看到。就不放gif了

makeClipRevealAnimation

官网的解释是 新的Activity从屏幕的一小块原始区域显示到其最终的完整表示

1
2
public static ActivityOptionsCompat makeClipRevealAnimation(@NonNull View source,
int startX, int startY, int width, int height)
  • source View:新活动从中进行动画处理的视图。这定义了startX和startY的坐标空间。
  • startX int: 新活动相对于source的x起始位置。
  • startY int: 活动的y起始位置,相对于source。
  • width int: 新活动的初始宽度。
  • height int: 新活动的初始高度。

这个比较迷惑,不知道那种场景适合用他

共享元素动画

上面他的几个方法,平常开发中用的不多。但是下面的几个方法,共享元素的,实际开发中,用的还是比较多的。

单个共享元素

1
2
public static ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity,
@NonNull View sharedElement, @NonNull String sharedElementName)

多个共享元素

1
2
public static ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity,
Pair<View, String>... sharedElements)

单个共享元素核心 在于 sharedElementName 在 两个不同界面 对需要共享的view 设置他的 sharedElementName 属性,当 sharedElementName 相同时候,触发共享元素动画

1
2
3
4
5
6
7
val intent = Intent(this, SecondAct::class.java)
val compat = ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
image,
"image_1_test"
)
ActivityCompat.startActivity(this, intent, compat.toBundle())

而对于多个共享元素,就需要创建 pair 即可 ,同样的需要在 第二个界面配置相同的 sharedElementName 属性

1
2
3
4
5
6
val intent = Intent(this, SecondAct::class.java)
val pair1 = androidx.core.util.Pair.create(image as View , "image_1_test")
val pair2 = androidx.core.util.Pair.create(btn_intent as View, "btn_intent")
val compat = ActivityOptionsCompat.makeSceneTransitionAnimation(this, pair1,pair2)

ActivityCompat.startActivity(this, intent, compat.toBundle())

5.0 以前设置转场动画

针对于 5.0 以前的 设备 可以使用 overridePendingTransition 实现 转场动画

  • 入场设置
1
2
3
4
startActivity(intent)
//参数1 进场动画 (B Activity 出现的动画)
//参数2 退场动画 (A Activity 消失的动画)
overridePendingTransition(R.anim.enter_in, R.anim.enter_out)
  • 退场设置
1
2
3
4
finish()
//参数1 进场动画 (A Activity 出现的动画)
//参数2 退场动画 (B Activity 消失的动画)
overridePendingTransition(R.anim.exit_in, R.anim.exit_out)
  • enter_in
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="100%"
android:toXDelta="0"
android:duration="3000">
</translate>
  • enter_out
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="0"
android:toXDelta="-100%"
android:duration="3000">
</translate>
  • exit_in
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="-100%"
android:toXDelta="0"
android:duration="3000">
</translate>
  • exit_out
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="0"
android:toXDelta="100%"
android:duration="3000">
</translate>

需要在 startActivity 或者 finish 之后立马执行