
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、Hero 动画系统架构深度解析
在现代移动应用中,页面转场动画是提升用户体验的重要手段。从简单的共享元素过渡到复杂的多元素联动,Flutter 提供了 Hero 组件来实现各种转场动画效果。理解这套架构的底层原理,是构建高性能转场系统的基础。
📱 1.1 Flutter Hero 架构
Flutter 的 Hero 动画系统由多个核心层次组成,每一层都有其特定的职责:
┌─────────────────────────────────────────────────────────────────┐
│ 应用层 (Application Layer) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Hero, HeroController, HeroMode... │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 动画层 (Animation Layer) │ │
│ │ AnimationController, Tween, CurvedAnimation... │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 转场层 (Transition Layer) │ │
│ │ HeroFlightShader, HeroFlight, RectTween... │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 导航层 (Navigation Layer) │ │
│ │ Navigator, Route, PageRoute, MaterialPageRoute... │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
🔬 1.2 Hero 核心组件详解
Flutter Hero 动画系统的核心组件包括以下几个部分:
Hero(英雄组件)
Hero 是实现共享元素转场的核心组件,通过 tag 标识匹配的元素。
dart
Hero(
tag: 'image-hero',
child: Image.network('https://example.com/image.jpg'),
)
Hero(
tag: 'image-hero',
child: Image.network('https://example.com/image.jpg'),
)
HeroController(英雄控制器)
HeroController 用于管理 Hero 动画的生命周期。
dart
MaterialApp(
navigatorObservers: [
HeroController(),
],
)
HeroFlight(英雄飞行)
HeroFlight 管理两个 Hero 之间的过渡动画。
dart
class CustomHeroFlight {
void start() {
// 开始飞行动画
}
void end() {
// 结束飞行动画
}
}
🎯 1.3 Hero 动画设计原则
设计优秀的 Hero 动画需要遵循以下原则:
┌─────────────────────────────────────────────────────────────┐
│ Hero 动画设计原则 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 视觉连续性 - 元素在页面间平滑过渡 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 2. 时间协调 - 动画时长与页面转场协调 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 3. 元素匹配 - 使用唯一的 tag 标识 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 4. 性能优化 - 避免复杂的 Hero 动画 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 5. 上下文相关 - 动画与内容相关联 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Hero 动画类型对比:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 基础 Hero | 简单位置过渡 | 图片详情页 |
| 自定义 Hero | 自定义动画效果 | 品牌定制 |
| 多元素 Hero | 多个元素联动 | 列表详情页 |
| 复杂转场 | 组合多种效果 | 特色页面 |
| 双向 Hero | 支持返回动画 | 详情页面 |
二、基础 Hero 动画实现
基础 Hero 动画包括简单共享元素、图片详情转场和卡片展开动画。这些是构建复杂转场系统的基础。
👆 2.1 简单共享元素动画
简单共享元素动画是最基础的 Hero 动画形式,实现元素在页面间的平滑过渡。
dart
import 'package:flutter/material.dart';
/// 简单共享元素动画示例
class SimpleHeroDemo extends StatelessWidget {
const SimpleHeroDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('简单 Hero 动画')),
body: Center(
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailPage(),
),
);
},
child: Hero(
tag: 'simple-hero',
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: const Center(
child: Text(
'点击查看详情',
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
),
),
),
),
);
}
}
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('详情页')),
body: Center(
child: Hero(
tag: 'simple-hero',
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.5),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: const Center(
child: Text(
'详情内容',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
),
);
}
}
🔄 2.2 图片详情转场
图片详情转场是 Hero 动画最常见的应用场景。
dart
/// 图片详情转场示例
class ImageHeroDemo extends StatelessWidget {
const ImageHeroDemo({super.key});
final List<String> images = const [
'https://picsum.photos/seed/1/400/300',
'https://picsum.photos/seed/2/400/300',
'https://picsum.photos/seed/3/400/300',
'https://picsum.photos/seed/4/400/300',
'https://picsum.photos/seed/5/400/300',
'https://picsum.photos/seed/6/400/300',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('图片 Hero 动画')),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: images.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ImageDetailPage(
imageUrl: images[index],
tag: 'image-$index',
),
),
);
},
child: Hero(
tag: 'image-$index',
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
images[index],
fit: BoxFit.cover,
),
),
),
);
},
),
);
}
}
class ImageDetailPage extends StatelessWidget {
final String imageUrl;
final String tag;
const ImageDetailPage({
super.key,
required this.imageUrl,
required this.tag,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.white),
),
body: GestureDetector(
onTap: () => Navigator.pop(context),
child: Center(
child: Hero(
tag: tag,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
),
),
),
),
);
}
}
🌊 2.3 卡片展开动画
卡片展开动画实现从列表卡片到详情页的平滑过渡。
dart
/// 卡片展开动画示例
class CardHeroDemo extends StatelessWidget {
const CardHeroDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('卡片 Hero 动画')),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 10,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CardDetailPage(index: index),
),
);
},
child: Hero(
tag: 'card-$index',
child: Card(
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
height: 120,
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.star,
color: Colors.primaries[index % Colors.primaries.length],
size: 40,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'卡片标题 ${index + 1}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'这是卡片的描述信息',
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
),
const Icon(Icons.chevron_right),
],
),
),
),
),
);
},
),
);
}
}
class CardDetailPage extends StatelessWidget {
final int index;
const CardDetailPage({super.key, required this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text('卡片 ${index + 1}'),
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: 'card-$index',
flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length].withOpacity(animation.value),
borderRadius: BorderRadius.circular(16),
),
);
},
);
},
child: Container(
width: double.infinity,
height: 150,
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Icon(
Icons.star,
color: Colors.primaries[index % Colors.primaries.length],
size: 60,
),
),
),
),
const SizedBox(height: 24),
const Text(
'详情内容',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text(
'这是卡片 ${index + 1} 的详细内容。Hero 动画让页面之间的过渡更加自然流畅。',
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
),
],
),
);
}
}
三、高级 Hero 动画实现
高级 Hero 动画包括自定义飞行动画、多元素联动、复杂转场效果和双向 Hero 动画。
📊 3.1 自定义飞行动画
自定义飞行动画通过 flightShuttleBuilder 实现独特的过渡效果。
dart
/// 自定义飞行动画示例
class CustomFlightDemo extends StatelessWidget {
const CustomFlightDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('自定义飞行动画')),
body: Center(
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CustomFlightDetailPage(),
),
);
},
child: Hero(
tag: 'custom-flight',
flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(animation.value * 3.14159),
alignment: Alignment.center,
child: Container(
decoration: BoxDecoration(
color: Color.lerp(Colors.blue, Colors.purple, animation.value),
borderRadius: BorderRadius.circular(12 + animation.value * 12),
boxShadow: [
BoxShadow(
color: Color.lerp(Colors.blue, Colors.purple, animation.value)!.withOpacity(0.5),
blurRadius: 10 + animation.value * 20,
offset: Offset(0, 5 + animation.value * 10),
),
],
),
),
);
},
);
},
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text('自定义飞行', style: TextStyle(color: Colors.white)),
),
),
),
),
),
);
}
}
class CustomFlightDetailPage extends StatelessWidget {
const CustomFlightDetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('详情页')),
body: Center(
child: Hero(
tag: 'custom-flight',
flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(animation.value * 3.14159),
alignment: Alignment.center,
child: Container(
decoration: BoxDecoration(
color: Color.lerp(Colors.purple, Colors.blue, animation.value),
borderRadius: BorderRadius.circular(24 - animation.value * 12),
),
),
);
},
);
},
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.purple,
borderRadius: BorderRadius.circular(24),
),
child: const Center(
child: Text('飞行完成', style: TextStyle(color: Colors.white, fontSize: 24)),
),
),
),
),
);
}
}
📝 3.2 多元素联动动画
多元素联动动画实现多个 Hero 元素的协同过渡。
dart
/// 多元素联动动画示例
class MultiHeroDemo extends StatelessWidget {
const MultiHeroDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('多元素联动')),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 5,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MultiHeroDetailPage(index: index),
),
);
},
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
),
],
),
child: Row(
children: [
Hero(
tag: 'avatar-$index',
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.primaries[index % Colors.primaries.length],
child: Text('${index + 1}'),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: 'title-$index',
flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
return DefaultTextStyle(
style: TextStyle(
fontSize: 14 + animation.value * 10,
fontWeight: FontWeight.bold,
color: Colors.black,
),
child: toHeroContext.widget,
);
},
child: Text(
'用户 ${index + 1}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 4),
Hero(
tag: 'subtitle-$index',
child: Text(
'这是用户的描述信息',
style: TextStyle(color: Colors.grey[600]),
),
),
],
),
),
],
),
),
);
},
),
);
}
}
class MultiHeroDetailPage extends StatelessWidget {
final int index;
const MultiHeroDetailPage({super.key, required this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 250,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
child: Center(
child: Hero(
tag: 'avatar-$index',
child: CircleAvatar(
radius: 50,
backgroundColor: Colors.white,
child: Text(
'${index + 1}',
style: const TextStyle(fontSize: 32),
),
),
),
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: 'title-$index',
child: const Text(
'',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
Text(
'用户 ${index + 1}',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Hero(
tag: 'subtitle-$index',
child: Text(
'',
style: TextStyle(color: Colors.grey[600]),
),
),
Text(
'这是用户的详细描述信息',
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 24),
const Text(
'详细内容',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'这是用户的详细内容。多元素 Hero 动画让页面之间的过渡更加自然流畅,多个元素协同运动,提供更好的视觉体验。',
),
],
),
),
),
],
),
);
}
}
🔄 3.3 复杂转场效果
复杂转场效果组合多种动画实现独特的过渡体验。
dart
/// 复杂转场效果示例
class ComplexTransitionDemo extends StatelessWidget {
const ComplexTransitionDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('复杂转场效果')),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: 6,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return ComplexDetailPage(index: index);
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOut),
),
child: child,
),
);
},
transitionDuration: const Duration(milliseconds: 500),
),
);
},
child: Hero(
tag: 'complex-$index',
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.star,
size: 40,
color: Colors.white.withOpacity(0.8),
),
const SizedBox(height: 8),
Text(
'项目 ${index + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
},
),
);
}
}
class ComplexDetailPage extends StatelessWidget {
final int index;
const ComplexDetailPage({super.key, required this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Hero(
tag: 'complex-$index',
child: Container(
width: double.infinity,
height: 300,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
),
),
SafeArea(
child: Column(
children: [
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
const SizedBox(height: 100),
Expanded(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'项目 ${index + 1}',
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'这是项目的详细描述',
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 24),
const Text(
'详细内容',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Expanded(
child: Text(
'复杂转场效果结合了 Hero 动画和自定义页面转场,提供更加丰富的视觉体验。',
),
),
],
),
),
),
),
],
),
),
],
),
);
}
}
四、完整示例:Hero 动画转场系统
下面是一个完整的 Hero 动画转场系统示例:
dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HeroHomePage(),
);
}
}
class HeroHomePage extends StatelessWidget {
const HeroHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('🦸 Hero 动画转场系统')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildSectionCard(context, title: '简单 Hero', description: '基础共享元素', icon: Icons.star, color: Colors.blue, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SimpleHeroDemo()))),
_buildSectionCard(context, title: '图片详情', description: '图片放大转场', icon: Icons.image, color: Colors.teal, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ImageHeroDemo()))),
_buildSectionCard(context, title: '卡片展开', description: '列表卡片转场', icon: Icons.view_agenda, color: Colors.green, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const CardHeroDemo()))),
_buildSectionCard(context, title: '自定义飞行', description: '自定义过渡效果', icon: Icons.flight, color: Colors.purple, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomFlightDemo()))),
_buildSectionCard(context, title: '多元素联动', description: '多个 Hero 协同', icon: Icons.people, color: Colors.orange, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MultiHeroDemo()))),
_buildSectionCard(context, title: '复杂转场', description: '组合动画效果', icon: Icons.auto_awesome, color: Colors.pink, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ComplexTransitionDemo()))),
],
),
);
}
Widget _buildSectionCard(BuildContext context, {required String title, required String description, required IconData icon, required Color color, required VoidCallback onTap}) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(width: 56, height: 56, decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)), child: Icon(icon, color: color, size: 28)),
const SizedBox(width: 16),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text(description, style: TextStyle(fontSize: 13, color: Colors.grey[600]))])),
Icon(Icons.chevron_right, color: Colors.grey[400]),
],
),
),
),
);
}
}
class SimpleHeroDemo extends StatelessWidget {
const SimpleHeroDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('简单 Hero 动画')),
body: Center(
child: GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const SimpleDetailPage())),
child: Hero(
tag: 'simple-hero',
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(12)),
child: const Center(child: Text('点击查看详情', style: TextStyle(color: Colors.white, fontSize: 16))),
),
),
),
),
);
}
}
class SimpleDetailPage extends StatelessWidget {
const SimpleDetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('详情页')),
body: Center(
child: Hero(
tag: 'simple-hero',
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(24)),
child: const Center(child: Text('详情内容', style: TextStyle(color: Colors.white, fontSize: 24))),
),
),
),
);
}
}
class ImageHeroDemo extends StatelessWidget {
const ImageHeroDemo({super.key});
final List<String> images = const [
'https://picsum.photos/seed/1/400/300',
'https://picsum.photos/seed/2/400/300',
'https://picsum.photos/seed/3/400/300',
'https://picsum.photos/seed/4/400/300',
'https://picsum.photos/seed/5/400/300',
'https://picsum.photos/seed/6/400/300',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('图片 Hero 动画')),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 1.2, crossAxisSpacing: 8, mainAxisSpacing: 8),
itemCount: images.length,
itemBuilder: (context, index) => GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => ImageDetailPage(imageUrl: images[index], tag: 'image-$index'))),
child: Hero(tag: 'image-$index', child: ClipRRect(borderRadius: BorderRadius.circular(12), child: Image.network(images[index], fit: BoxFit.cover))),
),
),
);
}
}
class ImageDetailPage extends StatelessWidget {
final String imageUrl;
final String tag;
const ImageDetailPage({super.key, required this.imageUrl, required this.tag});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0, iconTheme: const IconThemeData(color: Colors.white)),
body: GestureDetector(
onTap: () => Navigator.pop(context),
child: Center(child: Hero(tag: tag, child: Image.network(imageUrl, fit: BoxFit.contain))),
),
);
}
}
class CardHeroDemo extends StatelessWidget {
const CardHeroDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('卡片 Hero 动画')),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 10,
itemBuilder: (context, index) => GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => CardDetailPage(index: index))),
child: Hero(
tag: 'card-$index',
child: Card(
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
height: 120,
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(width: 80, height: 80, decoration: BoxDecoration(color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.2), borderRadius: BorderRadius.circular(12))),
const SizedBox(width: 16),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [Text('卡片标题 ${index + 1}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text('这是卡片的描述信息', style: TextStyle(color: Colors.grey[600]))])),
const Icon(Icons.chevron_right),
],
),
),
),
),
),
),
);
}
}
class CardDetailPage extends StatelessWidget {
final int index;
const CardDetailPage({super.key, required this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text('卡片 ${index + 1}'),
background: Container(decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.primaries[index % Colors.primaries.length], Colors.primaries[(index + 1) % Colors.primaries.length]]))),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: 'card-$index',
child: Container(
width: double.infinity,
height: 150,
decoration: BoxDecoration(color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.2), borderRadius: BorderRadius.circular(16)),
),
),
const SizedBox(height: 24),
const Text('详情内容', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Text('这是卡片 ${index + 1} 的详细内容。Hero 动画让页面之间的过渡更加自然流畅。', style: TextStyle(color: Colors.grey[600])),
],
),
),
),
],
),
);
}
}
class CustomFlightDemo extends StatelessWidget {
const CustomFlightDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('自定义飞行动画')),
body: Center(
child: GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const CustomFlightDetailPage())),
child: Hero(
tag: 'custom-flight',
flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) => AnimatedBuilder(
animation: animation,
builder: (context, child) => Transform(
transform: Matrix4.identity()..setEntry(3, 2, 0.001)..rotateY(animation.value * 3.14159),
alignment: Alignment.center,
child: Container(decoration: BoxDecoration(color: Color.lerp(Colors.blue, Colors.purple, animation.value), borderRadius: BorderRadius.circular(12 + animation.value * 12))),
),
),
child: Container(width: 150, height: 150, decoration: BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(12)), child: const Center(child: Text('自定义飞行', style: TextStyle(color: Colors.white)))),
),
),
),
);
}
}
class CustomFlightDetailPage extends StatelessWidget {
const CustomFlightDetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('详情页')),
body: Center(
child: Hero(
tag: 'custom-flight',
child: Container(width: 300, height: 300, decoration: BoxDecoration(color: Colors.purple, borderRadius: BorderRadius.circular(24)), child: const Center(child: Text('飞行完成', style: TextStyle(color: Colors.white, fontSize: 24)))),
),
),
);
}
}
class MultiHeroDemo extends StatelessWidget {
const MultiHeroDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('多元素联动')),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 5,
itemBuilder: (context, index) => GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => MultiHeroDetailPage(index: index))),
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10)]),
child: Row(
children: [
Hero(tag: 'avatar-$index', child: CircleAvatar(radius: 30, backgroundColor: Colors.primaries[index % Colors.primaries.length], child: Text('${index + 1}'))),
const SizedBox(width: 16),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Hero(tag: 'title-$index', child: Text('用户 ${index + 1}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold))), const SizedBox(height: 4), Hero(tag: 'subtitle-$index', child: Text('这是用户的描述信息', style: TextStyle(color: Colors.grey[600])))])),
],
),
),
),
),
);
}
}
class MultiHeroDetailPage extends StatelessWidget {
final int index;
const MultiHeroDetailPage({super.key, required this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 250,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.primaries[index % Colors.primaries.length], Colors.primaries[(index + 1) % Colors.primaries.length]])),
child: Center(child: Hero(tag: 'avatar-$index', child: CircleAvatar(radius: 50, backgroundColor: Colors.white, child: Text('${index + 1}', style: const TextStyle(fontSize: 32))))),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('用户 ${index + 1}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('这是用户的详细描述信息', style: TextStyle(color: Colors.grey[600])),
const SizedBox(height: 24),
const Text('详细内容', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('多元素 Hero 动画让页面之间的过渡更加自然流畅,多个元素协同运动,提供更好的视觉体验。'),
],
),
),
),
],
),
);
}
}
class ComplexTransitionDemo extends StatelessWidget {
const ComplexTransitionDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('复杂转场效果')),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 0.8, crossAxisSpacing: 16, mainAxisSpacing: 16),
itemCount: 6,
itemBuilder: (context, index) => GestureDetector(
onTap: () => Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => ComplexDetailPage(index: index),
transitionsBuilder: (context, animation, secondaryAnimation, child) => FadeTransition(opacity: animation, child: ScaleTransition(scale: Tween<double>(begin: 0.8, end: 1.0).animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)), child: child)),
transitionDuration: const Duration(milliseconds: 500),
),
),
child: Hero(
tag: 'complex-$index',
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.primaries[index % Colors.primaries.length], Colors.primaries[(index + 1) % Colors.primaries.length]]),
borderRadius: BorderRadius.circular(16),
),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.star, size: 40, color: Colors.white.withOpacity(0.8)), const SizedBox(height: 8), Text('项目 ${index + 1}', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold))]),
),
),
),
),
);
}
}
class ComplexDetailPage extends StatelessWidget {
final int index;
const ComplexDetailPage({super.key, required this.index});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Hero(tag: 'complex-$index', child: Container(width: double.infinity, height: 300, decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.primaries[index % Colors.primaries.length], Colors.primaries[(index + 1) % Colors.primaries.length]])))),
SafeArea(
child: Column(
children: [
AppBar(backgroundColor: Colors.transparent, elevation: 0, leading: IconButton(icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context))),
const SizedBox(height: 100),
Expanded(
child: Container(
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text('项目 ${index + 1}', style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text('这是项目的详细描述', style: TextStyle(color: Colors.grey[600])), const SizedBox(height: 24), const Text('详细内容', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 8), const Expanded(child: Text('复杂转场效果结合了 Hero 动画和自定义页面转场,提供更加丰富的视觉体验。'))]),
),
),
),
],
),
),
],
),
);
}
}
五、最佳实践与性能优化
🎨 5.1 性能优化建议
- 避免过多 Hero 元素:每个页面限制 Hero 元素数量
- 使用唯一的 tag:确保 Hero tag 在页面间唯一
- 优化动画时长:动画时长控制在 300-500ms
- 避免复杂布局:Hero 元素内部布局尽量简单
🔧 5.2 Hero 动画调试
dart
MaterialApp(
debugShowCheckedModeBanner: false,
navigatorObservers: [
HeroController(),
],
)
📱 5.3 OpenHarmony 适配
在 OpenHarmony 平台上,需要注意:
- 处理手势冲突
- 适配不同屏幕尺寸
- 优化动画性能
六、总结
本文详细介绍了 Flutter for OpenHarmony 的 Hero 动画转场系统,包括:
| 组件类型 | 核心技术 | 应用场景 |
|---|---|---|
| 简单 Hero | Hero + tag | 基础共享元素 |
| 图片详情 | Hero + Image | 图片放大转场 |
| 卡片展开 | Hero + Card | 列表详情页 |
| 自定义飞行 | flightShuttleBuilder | 品牌定制 |
| 多元素联动 | 多个 Hero + tag | 复杂页面 |
| 复杂转场 | Hero + PageRouteBuilder | 特色页面 |
参考资料
💡 提示:Hero 动画是提升应用用户体验的重要手段,合理使用可以让页面转场更加自然流畅。建议根据具体场景选择合适的 Hero 动画类型,并注意性能优化和动画时长控制。