Shared Element Transition — 一种动画,其中共享元素(图片、文本、卡片)在导航期间在两个屏幕之间平滑移动和变换。用户看到的不是突然的过渡,而是连续的运动:元素从屏幕 A 「流向」屏幕 B,保持上下文和视觉联系。根据 Material Design Guidelines(2026),Shared Element Transition 将感知性能提高 35%,并将导航理解度提高 28%。有关其他动画类型的更多信息,请阅读动画通用指南。
要点
Shared Element Transition — 是一种屏幕切换动画,其中一个或多个视觉元素从上一个屏幕「迁移」到下一个屏幕。在导航瞬间,系统在源屏幕上对元素进行快照,将其变换(位置、大小、形状、颜色)动画到新屏幕上的目标状态。因此,用户将两个屏幕感知为统一空间的一部分,而不是单独的页面。
主要使用场景:产品卡片 → 详情屏幕(图片放大并移到顶部),图片库 → 查看(图片平滑展开到全屏),联系人列表 → 个人资料(头像和姓名移动)。为了正常工作,两个屏幕必须具有相同 transitionName(Android)或快照标识符(iOS)的 View。
根据 Google I/O 2025,Shared Element Transition 是移动应用最有效的三种 UX 模式之一:如果用户看到共享元素的动画,他们在过渡后立即返回的可能性降低 22%。然而,过度使用(每次过渡超过 3 个共享元素)会降低效果,并可能导致中端设备的 FPS 下降到 40 以下。
在 Android 上,Shared Element Transition 通过 FragmentTransaction.addSharedElement(view, transitionName) 或 ActivityOptions.makeSceneTransitionAnimation 实现。每个 View 都被分配一个唯一的 android:transitionName — 在两个屏幕上相同。系统自动创建移动(ChangeBounds)、大小变化(ChangeTransform)和图片缩放(ChangeImageTransform)的动画。
<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" />
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 上没有 Android 的 sharedElementEnterTransition 的内置对应物 — 共享动画通过 UIViewControllerAnimatedTransitioning 使用 UIViewPropertyAnimator 或 CASpringAnimation 手动实现。主要技术:在 animateTransition(using:) 中获取源屏幕的 View,创建其快照,添加到 containerView,隐藏原始视图并将快照动画到目标位置。最后,显示目标 View 并移除快照。
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 Transition 通过 graphicsLayer 修饰符和 SharedTransitionScope(Compose 1.7+)动画实现。每个共享元素通过 sharedElement() 用唯一键标记。系统在 composable 函数之间的导航过程中自动动画化位置、大小、形状、颜色和裁剪蒙版的变化。根据 Android Developers Blog(2026),Compose Shared Element Transition 支持 AnimatedContent、NavHost 和自定义容器。
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 必须是两者的父级 |
// 优化:共享过渡中图像的占位符
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。
确保 transitionName 在 onBindViewHolder 中动态设置:holder.imageView.transitionName = «image_${position}»。对于 FragmentTransaction.addSharedElement,传递 holder 中的特定 View。对于 ActivityOptions,使用 Pair.create(holder.imageView, holder.imageView.transitionName)。通过 postponeEnterTransition() 和 startPostponedEnterTransition() 将共享过渡延迟到布局完成。
在 Android 上:将 ChangeBackground 添加到 TransitionSet,或使用覆盖 captureStartValues 和 captureEndValues 的自定义 Transition。在 iOS 上:动画化快照的 backgroundColor 或使用颜色变化动画化 UIViewPropertyAnimator。在 Jetpack Compose 中:Modifier.sharedElement 通过 Color 过渡自动动画化背景。对于卡片,还要动画化 elevation — 从列表中的 2dp 到详情屏幕上的 8dp。
是的,通过 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。
在 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。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。