Shared Element Transition — 是什么,过渡中的共享元素

作者: IT Sectr 发布日期: 2026-03-02 阅读时间: 10 分钟

Shared Element Transition — 一种动画,其中共享元素(图片、文本、卡片)在导航期间在两个屏幕之间平滑移动和变换。用户看到的不是突然的过渡,而是连续的运动:元素从屏幕 A 「流向」屏幕 B,保持上下文和视觉联系。根据 Material Design Guidelines(2026),Shared Element Transition 将感知性能提高 35%,并将导航理解度提高 28%。有关其他动画类型的更多信息,请阅读动画通用指南

要点

  • Shared Element Transition — 导航期间两个屏幕之间共享元素的动画。
  • sharedElementEnterTransition — Android 的 XML 属性,用于配置共享元素的进入。
  • UIViewPropertyAnimator — iOS 中 UIViewController 之间自定义共享动画的基础。
  • 共享元素通过 Android 中的 transitionName 和 iOS 中的 tag 或 snapshot 设置。
  • 动画适用于 ImageView、TextView、CardView、Button 以及任何具有唯一标识符的 View。

什么是 Shared Element Transition?

Shared Element Transition — 是一种屏幕切换动画,其中一个或多个视觉元素从上一个屏幕「迁移」到下一个屏幕。在导航瞬间,系统在源屏幕上对元素进行快照,将其变换(位置、大小、形状、颜色)动画到新屏幕上的目标状态。因此,用户将两个屏幕感知为统一空间的一部分,而不是单独的页面。

主要使用场景:产品卡片 → 详情屏幕(图片放大并移到顶部),图片库 → 查看(图片平滑展开到全屏),联系人列表 → 个人资料(头像和姓名移动)。为了正常工作,两个屏幕必须具有相同 transitionName(Android)或快照标识符(iOS)的 View。

根据 Google I/O 2025,Shared Element Transition 是移动应用最有效的三种 UX 模式之一:如果用户看到共享元素的动画,他们在过渡后立即返回的可能性降低 22%。然而,过度使用(每次过渡超过 3 个共享元素)会降低效果,并可能导致中端设备的 FPS 下降到 40 以下。

Android 中的 Shared Element Transition(Kotlin)

在 Android 上,Shared Element Transition 通过 FragmentTransaction.addSharedElement(view, transitionName)ActivityOptions.makeSceneTransitionAnimation 实现。每个 View 都被分配一个唯一的 android:transitionName — 在两个屏幕上相同。系统自动创建移动(ChangeBounds)、大小变化(ChangeTransform)和图片缩放(ChangeImageTransform)的动画。

xml

<androidx.cardview.widget.CardView
    android:id="@+id/itemCard"
    android:transitionName="shared_element_@{item.id}"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/itemImage"
        android:transitionName="shared_image_@{item.id}" />
</androidx.cardview.widget.CardView>


<ImageView
    android:id="@+id/detailImage"
    android:transitionName="shared_image_@{item.id}"
    android:layout_width="match_parent"
    android:layout_height="300dp" />
kotlin
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import androidx.fragment.app.FragmentTransaction

// Activity:通过 ActivityOptions 共享元素
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
    this,
    Pair(imageView, getString(R.string.transition_name_image)),
    Pair(titleView, getString(R.string.transition_name_title))
)
startActivity(intent, options.toBundle())

// Fragment:通过 FragmentTransaction 共享元素
val detailFragment = DetailFragment().apply {
    sharedElementEnterTransition = TransitionInflater.from(context)
        .inflateTransition(android.R.transition.move)
    sharedElementReturnTransition = TransitionInflater.from(context)
        .inflateTransition(android.R.transition.move)
}

supportFragmentManager.beginTransaction()
    .addSharedElement(imageView, imageView.transitionName)
    .addSharedElement(titleView, titleView.transitionName)
    .replace(R.id.container, detailFragment)
    .addToBackStack(null)
    .commit()

// 自定义过渡:ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
    ordering = TransitionSet.ORDERING_TOGETHER
    addTransition(ChangeBounds())
    addTransition(ChangeTransform())
    addTransition(ChangeImageTransform())
    duration = 400L
    interpolator = FastOutSlowInInterpolator()
}

重要 Android 细节:对于具有不同 scaleType(centerCrop → fitCenter)的 ImageView 的正确动画,请使用 ChangeImageTransform — 它会自动动画化显示方式的变化。对于具有圆角的 View(CardView),添加 ChangeBounds,它通过 outline 动画化圆角半径。对于文本动画(字体大小变化),使用 ChangeTextTransform。如果共享元素在其中一个屏幕上消失或出现 — 系统会自动应用 Fade。

iOS 中的 Shared Element Transition(Swift)

在 iOS 上没有 Android 的 sharedElementEnterTransition 的内置对应物 — 共享动画通过 UIViewControllerAnimatedTransitioning 使用 UIViewPropertyAnimator 或 CASpringAnimation 手动实现。主要技术:在 animateTransition(using:) 中获取源屏幕的 View,创建其快照,添加到 containerView,隐藏原始视图并将快照动画到目标位置。最后,显示目标 View 并移除快照。

swift
import UIKit

"> 自定义 Shared Element 动画器
class SharedElementAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    let fromView: UIView
    let toView: UIView
    let fromFrame: CGRect
    let toFrame: CGRect
    let isPresenting: Bool

    func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.4
    }

    func animateTransition(using ctx: UIViewControllerContextTransitioning) {
        guard let toVC = ctx.viewController(forKey: .to),
              let fromVC = ctx.viewController(forKey: .from)
        else { return }

        let container = ctx.containerView

        if isPresenting {
            container.addSubview(toVC.view)
            toVC.view.layoutIfNeeded()
            toVC.view.alpha = 0
        }

        // 共享元素的快照
        let snapshot = fromView.snapshotView(afterScreenUpdates: false)!
        snapshot.frame = isPresenting ? fromFrame : toFrame
        snapshot.layer.cornerRadius = isPresenting ? 12 : 0
        container.addSubview(snapshot)

        fromView.isHidden = true
        toView.isHidden = true

        UIView.animate(withDuration: transitionDuration(using: ctx),
                       delay: 0,
                       usingSpringWithDamping: 0.7,
                       initialSpringVelocity: 0) {
            snapshot.frame = self.isPresenting ? self.toFrame : self.fromFrame
            snapshot.layer.cornerRadius = self.isPresenting ? 0 : 12
            toVC.view.alpha = 1
        } completion: { _ in
            fromView.isHidden = false
            toView.isHidden = false
            snapshot.removeFromSuperview()
            ctx.completeTransition(!ctx.transitionWasCancelled)
        }
    }
}

// Hero — 简化共享过渡的库
// heroID 在两个屏幕上匹配,类似于 Android 中的 transitionName
import Hero

imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"

UIViewPropertyAnimator — 在 iOS 10+ 上进行共享动画的首选方式。与 UIView.animate 不同,PropertyAnimator 支持交互控制(fractionComplete),允许将共享过渡与滑动手势集成。对于 Hero 库(第三方),共享过渡通过 hero.id 设置 — 这是 Android 中 transitionName 的对应物,它会自动动画化屏幕之间的位置、大小、cornerRadius、zPosition 和 backgroundColor。

Jetpack Compose 中的 Shared Element

在 Jetpack Compose 中,Shared Element Transition 通过 graphicsLayer 修饰符和 SharedTransitionScope(Compose 1.7+)动画实现。每个共享元素通过 sharedElement() 用唯一键标记。系统在 composable 函数之间的导航过程中自动动画化位置、大小、形状、颜色和裁剪蒙版的变化。根据 Android Developers Blog(2026),Compose Shared Element Transition 支持 AnimatedContent、NavHost 和自定义容器。

kotlin
import androidx.compose.animation.core.spring
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut

// SharedTransitionLayout — 共享元素的容器
SharedTransitionLayout {
    val sharedTransitionState = remember { SharedTransitionState() }

    // 屏幕 A:列表
    LazyColumn {
        items(items) { item ->
            Card(
                modifier = Modifier.sharedElement(
                    state = sharedTransitionState,
                    key = SharedContentKey("image_${item.id}"),
                    boundsTransform = { _, _ ->
                        spring(dampingRatio = Spring.DampingRatioMediumBouncy)
                    }
                )
            ) {
                AsyncImage(model = item.imageUrl, contentDescription = null)
            }
        }
    }

    // 屏幕 B:详情(在选择元素时显示)
    AnimatedContent(
        targetState = selectedItem,
        transitionSpec = {
            EnterTransition.None using SizeTransform(clip = false)
        }
    ) { item ->
        item?.let {
            Image(
                modifier = Modifier.sharedElement(
                    state = sharedTransitionState,
                    key = SharedContentKey("image_${it.id}")
                ),
                painter = rememberAsyncImagePainter(it.imageUrl),
                contentDescription = null,
                contentScale = ContentScale.FillWidth
            )
        }
    }
}

Compose 1.7+ SharedTransitionScope — 在实验模式下可用的新 API。它取代了 NavHost 中共享动画的手动管理。关键功能:boundsTransform 用于自定义动画曲线,clipToBounds 用于圆角,zIndex 用于图层顺序。对于具有不同 contentScale 的图像的动画,使用带有 SharedContentKey 包装器的 Modifier.sharedElement。要支持较旧版本的 Compose(< 1.7),请使用 Google 的 SharedTransitionScope 库。

优化和典型问题

Shared Element Transition 如果不考虑平台限制,可能会导致性能问题。在 Android 上,主要问题是大图像(超过 2048 px)的 ChangeImageTransform。在 iOS 上 — 具有重型图层(CAGradientLayer、CAShapeLayer 带有许多节点)的快照。解决方案:对共享动画使用压缩图像,并使用 afterScreenUpdates: false 创建快照。

问题原因解决方案
动画卡顿过渡前后布局不同 / 快照过重使用 Placeholder:加载时显示小预览
共享元素不动画transitionName 在两个屏幕上不匹配检查 XML/代码中 transitionName 的精确匹配
Android 上 FPS < 40共享元素包含复杂的 View 层次结构仅共享 ImageView,而不是整个 CardView
iOS:快照伪影在具有蒙版/动画的图层上执行 renderInContext使用 drawHierarchy(in:afterScreenUpdates: true)
Compose:元素不出现SharedTransitionLayout 未包裹两个屏幕SharedTransitionLayout 必须是两者的父级
kotlin
// 优化:共享过渡中图像的占位符
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide

"> 导航前加载 preview
Glide.with(context)
    .load(imageUrl)
    .override(300, 300) // 共享过渡的大小限制
    .placeholder(R.drawable.placeholder)
    .into(imageView)

> 过渡后延迟加载 full-resolution
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
    Glide.with(context).load(imageUrl).into(detailImage)
}

// iOS:使用 drawHierarchy 优化快照
// snapshotView(afterScreenUpdates: false) — 更快,忽略待处理的更新
// 对于复杂视图:drawHierarchy(in: view.bounds, afterScreenUpdates: true)

最佳实践:每次过渡不要添加超过 3 个共享元素 — 每个额外元素都会增加渲染负载。对于列表(RecyclerView/UICollectionView),在被点击的项目上创建共享元素,而不是整个列表。对于带有圆角的卡片,在 Android 上使用 clipToOutline,在 iOS 上在快照中使用 cornerRadius。如果第二个屏幕上的共享元素位于 ScrollView 中,请等待布局完成(view.doOnPreDraw)再开始动画。

常见问题

一次过渡中可以动画多少个共享元素?

建议每次过渡不超过 3 个共享元素。每个额外元素都会增加渲染负载并可能导致 FPS 下降。最佳:1 张图片 + 1 段文本(标题)。对于 Android,每个元素通过 addSharedElement(view, transitionName) 添加。对于 iOS,每个快照单独添加到 containerView。

Shared Element Transition 不适用于 RecyclerView — 怎么办?

确保 transitionName 在 onBindViewHolder 中动态设置:holder.imageView.transitionName = «image_${position}»。对于 FragmentTransaction.addSharedElement,传递 holder 中的特定 View。对于 ActivityOptions,使用 Pair.create(holder.imageView, holder.imageView.transitionName)。通过 postponeEnterTransition() 和 startPostponedEnterTransition() 将共享过渡延迟到布局完成。

如何制作带有背景颜色变化的 Shared Element Transition?

在 Android 上:将 ChangeBackground 添加到 TransitionSet,或使用覆盖 captureStartValues 和 captureEndValues 的自定义 Transition。在 iOS 上:动画化快照的 backgroundColor 或使用颜色变化动画化 UIViewPropertyAnimator。在 Jetpack Compose 中:Modifier.sharedElement 通过 Color 过渡自动动画化背景。对于卡片,还要动画化 elevation — 从列表中的 2dp 到详情屏幕上的 8dp。

Shared Element 在 Android 上是否与 Jetpack Navigation 一起工作?

是的,通过 Navigation Component,Shared Element 通过 NavOptions 支持:val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment)。使用 FragmentNavigatorExtras 传递共享元素:FragmentNavigatorExtras(view to transitionName)。对于 Navigation Compose,使用来自 SharedTransitionScope(Compose 1.7+)的 Modifier.sharedElement。

如何调试 Shared Element Transition?

在 Android 上启用 Transition Debugging:adb shell setprop debug.transition 1 — 在 logcat 中会出现关于捕获和动画化共享元素的消息。在 Developer Options 中启用「Show layout bounds」以检查前后边界。在 iOS 上使用 Debug View Hierarchy 检查 containerView 中的快照。使用 Profile GPU Rendering(Android)或 Core Animation instrument(iOS)监控 FPS。

总结

  • Shared Element Transition — 屏幕之间共享元素的动画,保持视觉上下文。
  • Android:FragmentTransaction.addSharedElementActivityOptions.makeSceneTransitionAnimation
  • iOS:通过 UIViewControllerAnimatedTransitioning + 快照 + PropertyAnimator 进行自定义实现。
  • Jetpack Compose:来自 SharedTransitionScope(Compose 1.7+)的 Modifier.sharedElement
  • 每次过渡最多 3 个共享元素,以避免 FPS 下降。
  • 对于具有不同 scaleType 的 ImageView,使用 ChangeImageTransform(Android)或快照(iOS)。
  • Hero 库(iOS) — 具有 hero.id 的第三方解决方案,类似于 Android 中的 transitionName。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读