Blog #158: Activity转场动画

难度:⭐⭐

🎯 问题背景

默认的Activity切换效果单调乏味,无法提供良好的用户体验。精心设计的转场动画可以:

  • 提升应用的视觉体验
  • 引导用户注意力
  • 提供空间连续性和上下文
  • 让应用看起来更专业

Android提供了多种方式实现Activity转场动画。

💡 核心概念

转场动画类型

  1. 标准转场动画: overridePendingTransition()
  2. 共享元素转场: Shared Element Transition (Android 5.0+)
  3. Activity Transitions API: Material Design转场 (Android 5.0+)
  4. 自定义动画: 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")
        )
    }
}

⚡ 关键要点

  1. Android 5.0+特性: Material Design转场和共享元素转场需要API 21+
  2. transitionName: 共享元素需要在XML中设置transitionName属性
  3. postponeEnterTransition(): 异步加载数据时延迟转场动画
  4. 转场动画时长: 建议300-375ms,不宜过长
  5. 向后兼容: 低版本使用overridePendingTransition()降级
  6. 性能考虑: 复杂转场可能影响性能,注意优化
  7. FEATURE_CONTENT_TRANSITIONS: 使用Material转场前需要请求此特性

实际应用场景

在安防App中的应用:

  • 设备列表到详情: 共享元素转场(设备图标和名称)
  • 视频播放器全屏: Slide转场,从底部滑入
  • 设置页面: 标准右滑转场
  • 图片预览: 缩略图缩放动画

🔗 相关知识点

  • Blog #151: Activity生命周期详解
  • Blog #154: Activity跳转和数据传递
  • Material Motion: Material Design动效规范
  • MotionLayout: 复杂动画实现方案

标签: #Android #Activity #转场动画 #SharedElement

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐