Flutter多元素Hero返回动画起始位置异常问题求助
多元素Flutter Hero动画返回时不同步问题
正向动画中背景卡片和图片能正常缩放完成动画,但返回时图片会突然跳转到中心,动画效果远不及正向流畅。如何让返回时两者同步回到网格中的原始位置,实现和正向动画一致的效果?
代码实现
main.dart
import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_playground/screen_two.dart'; import 'package:flutter_playground/transition/hero_dialog_route.dart'; import 'package:flutter_playground/transition/hero_page_route.dart'; import 'package:flutter_playground/transition_open_container_wrapper.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 1, crossAxisSpacing: 4, mainAxisSpacing: 4, ), shrinkWrap: true, itemCount: 100, itemBuilder: (context, index) { return getChild(index); }), ); } Widget getChild(int index) { return GestureDetector( onTap: () { Navigator.push( context, HeroDialogRoute(builder: (context) => ScreenTwo(index: index)), ); }, child: getCard(index), ); } Widget getCard2(int index) { return OpenContainerWrapper( isRootNavigator: true, closedBuilder: (context, voidCallback) { return InkWell( onTap: voidCallback, child: getCard(index), ); }, openBuilder: (context, voidCallback) { return Container(height: 300, width: 300, child: ScreenTwo(index: index),); }, transitionType: ContainerTransitionType.fade, onClosed: (value) {}, ); } Widget getCard(int index) { return Stack( children: [ Hero( createRectTween: (Rect? begin, Rect? end) { return CurvedRectArcTween(begin: begin, end: end); }, tag: 'hero_card_${index}', child: Card( child: SizedBox( height: double.infinity, width: double.infinity, ), ), ), Hero( tag: 'hero_image_${index}', createRectTween: (Rect? begin, Rect? end) { return CurvedRectArcTween(begin: begin, end: end); }, child: Container( padding: EdgeInsets.all(8), child: Image.network('https://picsum.photos/200', fit: BoxFit.contain,), height: double.infinity, width: double.infinity, ), ) ], ); } }
screen_two.dart
import 'dart:ui'; import 'package:flutter/material.dart'; class ScreenTwo extends StatefulWidget { final int index; const ScreenTwo({super.key, required this.index}); @override State<StatefulWidget> createState() { return _ScreenTwoState(); } } class _ScreenTwoState extends State<ScreenTwo> { @override Widget build(BuildContext context) { return GestureDetector( onTap: () { Navigator.pop(context); }, child: Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Scaffold( appBar: AppBar( automaticallyImplyLeading: false, elevation: 0, backgroundColor: Colors.transparent, ), backgroundColor: Colors.transparent, body: BackdropFilter( filter: ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Center( child: getContent(), )), ), ), ); } Widget getContent() { return Stack( children: [ Hero( tag: 'hero_card_${widget.index}', createRectTween: (Rect? begin, Rect? end) { return RectTween(begin: begin, end: end); }, child: Container( width: 416, height: 416, ), ), Padding( padding: EdgeInsets.all(8), child: Hero( tag: 'hero_image_${widget.index}', createRectTween: (Rect? begin, Rect? end) { return RectTween(begin: begin, end: end); }, child: Card( child: ListView( shrinkWrap: true, children: [ Padding( padding: EdgeInsets.all(8), child: Image.network( 'https://picsum.photos/200', fit: BoxFit.contain, ), ), Container( height: 200, ) ], ), ), ), ) ], ); } }
hero_page_route.dart
import 'package:flutter/material.dart'; class CurvedRectArcTween extends RectTween { late double a; late double b; late double c; late double d; CurvedRectArcTween({ Rect? begin, Rect? end, double? a, double? b, double? c, double? d, }) : super(begin: begin, end: end) { this.a = a ?? 0; this.b = b ?? 0; this.c = c ?? 0; this.d = d ?? 0; } @override Rect? lerp(double t) { Cubic easeInOut = Cubic(a, b , c, d); double curvedT = easeInOut.transform(t); return super.lerp(curvedT); } }
hero_dialog_route.dart
import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; class HeroDialogRoute<T> extends PageRoute<T> { final WidgetBuilder builder; HeroDialogRoute({required this.builder}) : super(); @override bool get opaque => false; @override bool get barrierDismissible => true; @override Duration get transitionDuration => const Duration(milliseconds: 1000); @override bool get maintainState => true; @override Color get barrierColor => Colors.black54; @override Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { return FadeScaleTransition( animation: animation, child: child, ); } @override Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return builder(context); } @override String? get barrierLabel => null; }
transition_open_container_wrapper.dart
class OpenContainerWrapper extends StatelessWidget { const OpenContainerWrapper({ Key? key, required this.isRootNavigator, required this.closedBuilder, required this.openBuilder, required this.transitionType, required this.onClosed, }) :super(key: key); final CloseContainerBuilder closedBuilder; final OpenContainerBuilder<bool?> openBuilder; final ContainerTransitionType transitionType; final ClosedCallback<bool?> onClosed; final bool isRootNavigator; @override Widget build(BuildContext context) { return OpenContainer<bool>( useRootNavigator: isRootNavigator, closedElevation: 0, openColor: Colors.transparent, transitionDuration: const Duration(milliseconds: 450), closedShape: const RoundedRectangleBorder(), openShape: const RoundedRectangleBorder(), transitionType: transitionType, openBuilder: openBuilder, onClosed: onClosed, tappable: false, closedBuilder: closedBuilder, ); } }
问题原因与修复方案
核心问题
- RectTween不统一:首页Hero使用自定义的
CurvedRectArcTween,但第二页使用默认RectTween,导致正向/反向动画插值曲线不匹配。 - 布局约束不一致:首页图片Hero是带padding的Container包裹Image,第二页是Card+ListView+多层Padding的嵌套结构,布局边界计算差异导致返回时位置偏移。
- 路由过渡冲突:
HeroDialogRoute添加的FadeScaleTransition会干扰Hero动画的原生过渡逻辑。
修复步骤
1. 统一全局Hero的RectTween
修改ScreenTwo的getContent方法,让两个Hero都使用自定义的CurvedRectArcTween:
Widget getContent() { return Stack( children: [ Hero( tag: 'hero_card_${widget.index}', createRectTween: (Rect? begin, Rect? end) { return CurvedRectArcTween(begin: begin, end: end); }, child: Container( width: 416, height: 416, ), ), Padding( padding: EdgeInsets.all(8), child: Hero( tag: 'hero_image_${widget.index}', createRectTween: (Rect? begin, Rect? end) { return CurvedRectArcTween(begin: begin, end: end); }, child: // 后续布局调整 ), ) ], ); }
2. 对齐图片Hero的布局约束
调整第二页图片Hero的结构,和首页保持一致的padding与布局边界:
Hero( tag: 'hero_image_${widget.index}', createRectTween: (Rect? begin, Rect? end) { return CurvedRectArcTween(begin: begin, end: end); }, child: Container( padding: EdgeInsets.all(8), // 和首页保持相同padding height: double.infinity, width: double.infinity, child: Card( child: ListView( shrinkWrap: true, children: [ Image.network( 'https://picsum.photos/200', fit: BoxFit.contain, ), Container(height: 200), ], ), ), ), )
3. 移除路由的额外过渡效果
修改HeroDialogRoute的buildTransitions方法,让Hero动画主导页面过渡:
@override Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { return child; // 移除FadeScaleTransition }
内容的提问来源于stack exchange,提问作者Ali Yucel Akgul
相关产品推荐
相关产品推荐

