我正在构建一个带有
Hero
动画的应用程序,可以从主屏幕转换到第二个屏幕。这是我的代码的简化版本:
// A single tile, placed in a grid
class BeverageTile extends StatelessWidget {
final Beverage beverage;
const BeverageTile({
super.key,
required this.beverage,
});
@override
Widget build(BuildContext context) {
final String price = beverage.price == null ? 'Free' : '\$${beverage.price}';
return GestureDetector(
onTap: () => Navigator.of(context).push(
FadePageRoute(
page: BeverageSelectionScreen(beverage: beverage),
),
),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(16.0),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 6.0,
),
child: Text(
price,
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w700,
),
),
),
),
],
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 16.0,
right: 16.0,
),
// SETUP OF THE HERO
child: Hero(
createRectTween: (begin, end) {
return RectTween(begin: begin, end: end);
},
tag: 'beverage_${beverage.name}',
child: SvgPicture.asset(
beverage.imagePath,
fit: BoxFit.contain,
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 8.0,
),
child: Text(
beverage.name,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16.0),
),
),
],
),
),
);
}
}
// Second screen
class BeverageSelectionScreen extends StatelessWidget {
final Beverage beverage;
const BeverageSelectionScreen({super.key, required this.beverage});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFF1a1718),
),
child: Padding(
padding: const EdgeInsets.all(72.0),
child: Hero(
tag: 'beverage_${beverage.name}',
child: SvgPicture.asset(
beverage.imagePath,
fit: BoxFit.contain,
),
),
),
);
}
}
我还实现了一个自定义 PageRouteBuilder 来处理页面之间的过渡动画。 Hero 动画按预期工作,但图像当前遵循弯曲路径,这是根据 Material Design 运动规范的默认行为。
问题:如何自定义英雄动画的路径,让图像沿着直线路径移动,而不是默认的曲线轨迹?
默认情况下,
Hero
使用MaterialRectArcTween
,它会创建弯曲的动画路径。要使英雄遵循线性路径,请在 RectTween
属性中使用 createRectTween
:
createRectTween: (begin, end) => RectTween(begin: begin, end: end),
重要:
确保覆盖两个
createRectTween
小部件(两个页面上)中的 Hero
,以确保反向动画也是线性的。
完整示例:
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() {
// Slow down the animation to better see the Hero flight.
timeDilation = 15.0;
runApp(
const MaterialApp(
home: HeroLinearPathExample(),
),
);
}
class HeroLinearPathExample extends StatelessWidget {
const HeroLinearPathExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Linear Hero Animation Example'),
),
body: Align(
alignment: Alignment.bottomLeft,
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const SecondPage(),
),
);
},
child: Hero(
tag: 'hero-tag',
createRectTween: (begin, end) {
// Enforce a linear animation
return RectTween(begin: begin, end: end);
},
child: const FlutterLogo(size: 100),
),
),
),
);
}
}
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Page'),
),
body: Align(
alignment: Alignment.topRight,
child: Hero(
tag: 'hero-tag',
createRectTween: (begin, end) {
// Enforce a linear animation for the reverse transition
return RectTween(begin: begin, end: end);
},
child: const FlutterLogo(size: 100),
),
),
);
}
}