Blog #158: Activity转场动画
{// 圆形揭露动画 (Android 5.0+)view,0,0,// 缩放动画view,0,0,// 缩略图动画view,thumbnail,0,0。
·
Blog #158: Activity转场动画
难度:⭐⭐
🎯 问题背景
默认的Activity切换效果单调乏味,无法提供良好的用户体验。精心设计的转场动画可以:
- 提升应用的视觉体验
- 引导用户注意力
- 提供空间连续性和上下文
- 让应用看起来更专业
Android提供了多种方式实现Activity转场动画。
💡 核心概念
转场动画类型
- 标准转场动画: overridePendingTransition()
- 共享元素转场: Shared Element Transition (Android 5.0+)
- Activity Transitions API: Material Design转场 (Android 5.0+)
- 自定义动画: Animation/Animator
Material Design转场类型
- Explode: 元素从屏幕中心飞入/飞出
- Slide: 元素从屏幕边缘滑入/滑出
- Fade: 淡入/淡出效果
代码示例
1. 标准转场动画
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupTransitions()
}
private fun setupTransitions() {
// 1. 从右侧滑入
binding.btnSlideIn.setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)
}
// 2. 淡入淡出
binding.btnFade.setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
}
// 3. 从下方滑入
binding.btnSlideUp.setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
overridePendingTransition(R.anim.slide_in_bottom, R.anim.slide_out_top)
}
// 4. 缩放动画
binding.btnScale.setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
overridePendingTransition(R.anim.scale_in, R.anim.scale_out)
}
}
override fun finish() {
super.finish()
// 返回时的动画
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
}
}
<!-- res/anim/slide_in_right.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="300"
android:fromXDelta="100%"
android:toXDelta="0%" />
</set>
<!-- res/anim/slide_out_left.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="300"
android:fromXDelta="0%"
android:toXDelta="-100%" />
</set>
<!-- res/anim/fade_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:fromAlpha="0.0"
android:toAlpha="1.0" />
<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:fromAlpha="1.0"
android:toAlpha="0.0" />
<!-- res/anim/scale_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="300"
android:fromXScale="0.0"
android:fromYScale="0.0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.0"
android:toYScale="1.0" />
<alpha
android:duration="300"
android:fromAlpha="0.0"
android:toAlpha="1.0" />
</set>
<!-- res/anim/scale_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="300"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="0.0"
android:toYScale="0.0" />
<alpha
android:duration="300"
android:fromAlpha="1.0"
android:toAlpha="0.0" />
</set>
2. Material Design转场动画
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 在setContentView之前启用转场动画
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
}
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupMaterialTransitions()
}
private fun setupMaterialTransitions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// 1. Explode动画
binding.btnExplode.setOnClickListener {
val intent = Intent(this, DetailActivity::class.java)
val options = ActivityOptions.makeSceneTransitionAnimation(this)
startActivity(intent, options.toBundle())
}
// 2. Slide动画
binding.btnSlide.setOnClickListener {
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("transition_type", "slide")
val options = ActivityOptions.makeSceneTransitionAnimation(this)
startActivity(intent, options.toBundle())
}
// 3. Fade动画
binding.btnFade.setOnClickListener {
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("transition_type", "fade")
val options = ActivityOptions.makeSceneTransitionAnimation(this)
startActivity(intent, options.toBundle())
}
}
}
}
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 配置转场动画
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
when (intent.getStringExtra("transition_type")) {
"explode" -> {
window.enterTransition = Explode()
window.exitTransition = Explode()
}
"slide" -> {
window.enterTransition = Slide(Gravity.END)
window.exitTransition = Slide(Gravity.START)
}
"fade" -> {
window.enterTransition = Fade()
window.exitTransition = Fade()
}
}
// 设置转场动画时长
window.enterTransition?.duration = 300
window.exitTransition?.duration = 300
}
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
3. 共享元素转场(Shared Element Transition)
<!-- MainActivity布局 -->
<androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/ivDevice"
android:layout_width="80dp"
android:layout_height="80dp"
android:transitionName="device_image"
android:src="@drawable/ic_camera" />
<TextView
android:id="@+id/tvDeviceName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:transitionName="device_name"
android:text="摄像头" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- DetailActivity布局 -->
<androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/ivDeviceDetail"
android:layout_width="200dp"
android:layout_height="200dp"
android:transitionName="device_image"
android:src="@drawable/ic_camera" />
<TextView
android:id="@+id/tvDeviceNameDetail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:transitionName="device_name"
android:text="摄像头"
android:textSize="24sp" />
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupSharedElementTransition()
}
private fun setupSharedElementTransition() {
binding.deviceCard.setOnClickListener {
val intent = Intent(this, DetailActivity::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// 单个共享元素
val options = ActivityOptions.makeSceneTransitionAnimation(
this,
binding.ivDevice,
"device_image"
)
startActivity(intent, options.toBundle())
} else {
startActivity(intent)
}
}
// 多个共享元素
binding.deviceCard2.setOnClickListener {
val intent = Intent(this, DetailActivity::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val options = ActivityOptions.makeSceneTransitionAnimation(
this,
Pair(binding.ivDevice, "device_image"),
Pair(binding.tvDeviceName, "device_name")
)
startActivity(intent, options.toBundle())
} else {
startActivity(intent)
}
}
}
}
class DetailActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// 配置共享元素转场
window.sharedElementEnterTransition = TransitionSet().apply {
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 375
}
window.sharedElementReturnTransition = TransitionSet().apply {
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 375
}
}
}
}
4. 自定义转场动画
class CustomTransitionActivity : AppCompatActivity() {
private lateinit var binding: ActivityCustomTransitionBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCustomTransitionBinding.inflate(layoutInflater)
setContentView(binding.root)
// 圆形揭露动画 (Android 5.0+)
binding.btnCircularReveal.setOnClickListener { view ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(this, DetailActivity::class.java)
val options = ActivityOptions.makeClipRevealAnimation(
view,
0,
0,
view.width,
view.height
)
startActivity(intent, options.toBundle())
}
}
// 缩放动画
binding.btnScale.setOnClickListener { view ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(this, DetailActivity::class.java)
val options = ActivityOptions.makeScaleUpAnimation(
view,
0,
0,
view.width,
view.height
)
startActivity(intent, options.toBundle())
}
}
// 缩略图动画
binding.ivThumbnail.setOnClickListener { view ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val thumbnail = (view as ImageView).drawable.toBitmap()
val intent = Intent(this, DetailActivity::class.java)
val options = ActivityOptions.makeThumbnailScaleUpAnimation(
view,
thumbnail,
0,
0
)
startActivity(intent, options.toBundle())
}
}
}
}
5. 延迟共享元素转场
class DetailActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
// 延迟转场动画,直到图片加载完成
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
postponeEnterTransition()
}
// 使用Glide加载图片
Glide.with(this)
.load(imageUrl)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
startPostponedEnterTransition()
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
startPostponedEnterTransition()
return false
}
})
.into(binding.ivDeviceDetail)
// 或使用ViewTreeObserver
binding.ivDeviceDetail.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
binding.ivDeviceDetail.viewTreeObserver.removeOnPreDrawListener(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
startPostponedEnterTransition()
}
return true
}
}
)
}
}
6. 转场动画工具类
object TransitionHelper {
/**
* 标准滑动转场
*/
fun slideTransition(activity: Activity, intent: Intent) {
activity.startActivity(intent)
activity.overridePendingTransition(
R.anim.slide_in_right,
R.anim.slide_out_left
)
}
/**
* 淡入淡出转场
*/
fun fadeTransition(activity: Activity, intent: Intent) {
activity.startActivity(intent)
activity.overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
}
/**
* 共享元素转场
*/
fun sharedElementTransition(
activity: Activity,
intent: Intent,
vararg sharedElements: Pair<View, String>
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val options = ActivityOptions.makeSceneTransitionAnimation(
activity,
*sharedElements
)
activity.startActivity(intent, options.toBundle())
} else {
activity.startActivity(intent)
}
}
/**
* 圆形揭露转场
*/
fun circularRevealTransition(
activity: Activity,
intent: Intent,
view: View
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val options = ActivityOptions.makeClipRevealAnimation(
view, 0, 0, view.width, view.height
)
activity.startActivity(intent, options.toBundle())
} else {
activity.startActivity(intent)
}
}
/**
* 配置Material Design转场
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun setupMaterialTransition(
window: Window,
enterTransition: Transition = Fade(),
exitTransition: Transition = Fade()
) {
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
window.enterTransition = enterTransition.apply { duration = 300 }
window.exitTransition = exitTransition.apply { duration = 300 }
}
}
// 使用示例
class MainActivity : AppCompatActivity() {
private fun navigateToDetail(device: Device, imageView: ImageView) {
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("device", device)
TransitionHelper.sharedElementTransition(
this,
intent,
Pair(imageView, "device_image")
)
}
}
⚡ 关键要点
- Android 5.0+特性: Material Design转场和共享元素转场需要API 21+
- transitionName: 共享元素需要在XML中设置transitionName属性
- postponeEnterTransition(): 异步加载数据时延迟转场动画
- 转场动画时长: 建议300-375ms,不宜过长
- 向后兼容: 低版本使用overridePendingTransition()降级
- 性能考虑: 复杂转场可能影响性能,注意优化
- FEATURE_CONTENT_TRANSITIONS: 使用Material转场前需要请求此特性
实际应用场景
在安防App中的应用:
- 设备列表到详情: 共享元素转场(设备图标和名称)
- 视频播放器全屏: Slide转场,从底部滑入
- 设置页面: 标准右滑转场
- 图片预览: 缩略图缩放动画
🔗 相关知识点
- Blog #151: Activity生命周期详解
- Blog #154: Activity跳转和数据传递
- Material Motion: Material Design动效规范
- MotionLayout: 复杂动画实现方案
标签: #Android #Activity #转场动画 #SharedElement
更多推荐


所有评论(0)