要点
Hero Animation — 一种界面动画技术,其中共享元素从源屏幕平滑移动到目标屏幕,同时保持其身份。这个名称反映了“英雄”的隐喻——关键元素在屏幕之间经历场景过渡,吸引用户的注意力。与标准过渡不同,Hero Animation不会断开视觉连接:用户可以看到元素的连续轨迹,从而更好地理解导航层次结构。
Hero Animation的概念在Material Design中作为列表-详情屏幕、画廊和卡片的推荐而被确立。动画算法由三个阶段组成:在源屏幕上,元素被高亮并固定在绝对位置;在过渡期间,它根据目标容器进行缩放、移动和改变形状;在最终屏幕上,元素被集成到新的布局中。根据Material Design 3,正确实现的英雄动画可将错误的“返回”点击次数减少22%。
在Flutter中,Hero Animation通过Hero小部件实现,它接受必需参数tag——一个唯一标识符。Flutter自动在源和目标路由上找到具有相同标签的Hero配对并启动动画。Hero小部件不需要手动动画管理——框架通过内部FlightShuttle对象自行计算轨迹并应用变换。
FlightShuttle — 一个不可见的“穿梭机”,在过渡瞬间创建Hero小部件的叠加副本,将其放置在绝对坐标系中,并从起始位置到结束位置动画化变换(scale、translate、rotate)。可以通过flightShuttleBuilder属性将自定义曲线传递给FlightShuttle,从而控制飞行轨迹:沿弧线飞行、从边界反弹或在到达前减速。在Flutter 3.16+中,Hero支持保持clipRect和borderRadius的动画,产生圆角卡片转换为矩形屏幕的效果。
在SwiftUI中,Hero Animation的等效物是matchedGeometryEffect修饰符,在iOS 14中引入。它通过标识符(id)和命名空间(namespace)连接两个元素。与Flutter中Hero自动拦截过渡不同,SwiftUI需要显式声明命名空间并将修饰符应用于场景的两个部分——源和目标——在同一层次结构内或通过GeometryReader。
matchedGeometryEffect动画支持位置、大小、旋转和圆角半径(cornerRadius)的变换。为了平滑,在withAnimation中设置Animation。SwiftUI根据默认的easeInOut曲线自动插值中间状态。在iOS 17+中,为TabView和NavigationStack添加了matchedTransitionSource支持——Hero Animation的系统级对应物,具有预定义的标准过渡轨迹。
在Android中,Hero Animation被称为Shared Element Transition,通过ActivityOptions.makeSceneTransitionAnimation或FragmentTransaction.addSharedElement实现。每个共享元素在XML中或通过代码使用transitionName属性标记,Android负责活动之间的动画。从Android 5.0(API 21)开始,过渡成为框架的一部分;在此之前,必须通过ValueAnimator编写自定义动画。
在Jetpack Compose中,Shared Element Transition通过Modifier.sharedElement和Modifier.sharedBounds修饰符结合AnimatedContent或SharedTransitionLayout实现(从Compose BOM 2024.01起可用)。与View系统不同,Compose懒重绘元素,因此共享元素缓存在CompositionLocal中。Compose还支持列表到详情(list→detail)的动画,同时保持scrollOffset——Hero过渡的关键能力。根据Google I/O 2024,由于直接管理RenderNode,Compose Shared Element Transition比View版本快15%。
Flutter中Hero Animation的基本示例:列表屏幕上的缩略图和详情屏幕上的全屏图像。标签image-hero连接两个小部件,FlightShuttle自动动画化从卡片到全屏的过渡。
import 'package:flutter/material.dart';
class ListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
Hero(
tag: 'image-hero',
child: GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(
builder: (_) => DetailScreen(),
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'https://picsum.photos/200',
width: 100, height: 100,
fit: BoxFit.cover,
),
),
),
),
],
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail')),
body: Center(
child: Hero(
tag: 'image-hero',
child: Image.network(
'https://picsum.photos/800',
width: 300, height: 400,
fit: BoxFit.cover,
),
),
),
);
}
}
关键点——两个屏幕上相同的标签(image-hero)。Flutter在路由过渡期间根据标签匹配Hero小部件。如果标签不同,动画将不会启动——元素只会出现在新屏幕上而没有过渡。任何类型都可以用作标签,但字符串是最可靠的选择。
在SwiftUI中使用NavigationStack的Hero Animation示例。matchedGeometryEffect修饰符通过命名空间连接列表和详情屏幕上的图像,withAnimation启动平滑过渡。
import SwiftUI
struct HeroExampleView: View {
@Namespace private var heroNamespace
@State private var isDetailShown = false
var body: some View {
VStack {
if isDetailShown {
DetailView(namespace: heroNamespace)
.transition(.identity)
} else {
ListView(namespace: heroNamespace)
.transition(.identity)
}
}
.animation(Animation.easeInOut(duration: 0.4),
value: isDetailShown)
}
}
struct ListView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 100, height: 100)
.cornerRadius(12)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
struct DetailView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 300, height: 400)
.cornerRadius(0)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
@Namespace为matchedGeometryEffect标识符创建命名空间。两个元素使用相同的id(hero)和共享的命名空间。当isDetailShown切换时,SwiftUI动画化两个视图之间的尺寸、位置和cornerRadius的变换,产生英雄效果。对于iOS 16+的标准NavigationLink,使用navigationTransition(.hero)。
使用SharedTransitionLayout在Jetpack Compose中的Shared Element Transition示例。sharedElement修饰符在导航期间动画化屏幕之间的共享元素。
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun HeroExample() {
SharedTransitionLayout {
var isDetail by remember { mutableStateOf(false) }
AnimatedContent(
targetState = isDetail,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}
) { detail ->
if (detail) {
Box(
modifier = Modifier
.fillMaxSize()
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/800",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
} else {
Box(
modifier = Modifier
.size(100.dp)
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/200",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
}
Compose SharedTransitionLayout(Compose BOM 2024.01+)包装层次结构,带有hero键的sharedElement修饰符连接开始和结束元素。AnimatedContent切换状态,Compose自动动画化图像的大小、位置和形状。关键优势——支持bounds和clipBounds动画,在过渡期间产生卡片圆角的效果。
常见问题
是的,Hero Animation在两个平台上都可用。在iOS中,使用SwiftUI中的matchedGeometryEffect或UINavigationController.transitioningDelegate。在Android中——ActivityOptions.makeSceneTransitionAnimation过渡或Jetpack Compose中使用Modifier.sharedElement的Shared Element Transition。Flutter通过Hero小部件实现Hero Animation,在两个平台上工作完全相同。
是的,Hero Animation支持多个共享元素。在Flutter中指定多个具有不同标签的Hero。在SwiftUI中,在每个元素上使用具有不同标识符的matchedGeometryEffect。所有动画并行执行——每个元素沿自己的轨迹移动。
性能取决于实现。Flutter FlightShuttle通过RenderObject处理图像,提供60fps。SwiftUI matchedGeometryEffect是矢量化的。对于大图像,在Flutter中使用precacheImage或在SwiftUI中使用prefetch——这消除了第一次过渡时的延迟。Android Compose需要通过AsyncImage进行懒加载。
标准的push过渡移动整个屏幕,产生卡片堆叠的错觉。Hero Animation将视觉注意力固定在一个元素上——用户看到它的连续运动,从而更好地理解屏幕之间的关系。Push适合层次导航,Hero适合强调内容。
不需要,Hero Animation是使用内置工具实现的。在Flutter中,它是来自flutter/material.dart包的Hero小部件。在SwiftUI中——matchedGeometryEffect修饰符。在Android Jetpack Compose中——来自标准Compose的SharedTransitionLayout和Modifier.sharedElement。额外的库仅在非常规轨迹或复杂变换时才需要。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。