Реверсивная анимация нескольких элементов Flutter Hero начинается в центре
Я пытаюсь создать анимацию героя с несколькими элементами, пока мой код выглядит следующим образом:
основной.дротик:
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});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
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:
import 'dart:ui';
import 'package:flutter/material.dart';
/// Created by ali on 12/19/22.
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:
import 'package:flutter/material.dart';
/// Created by ali on 12/24/22.
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';
/// Created by ali on 11/22/22.
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,
);
}
}
Вот gif для визуализации того, что делает код: https://i.imgur.com/xRcRWzH.mp4
Как вы можете видеть, когда герой начинает с фоновой карты и изображения и хорошо масштабируется, пока анимация не закончится. Но при возврате назад изображение резко прыгает в центр и анимация возврата выглядит не так хорошо, как анимация начала. Как заставить эти два элемента работать вместе при возвращении на первые позиции в сетке, как это происходит в первой анимации?
Заранее спасибо.
1 ответ
Я упростил ваш пример, исправив некоторые части макета, а также я не используюanimation
package, чтобы сделать этот эффект перехода.
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,
PageRouteBuilder(
opaque: false,
barrierColor: Colors.black38,
transitionDuration: const Duration(seconds: 1),
reverseTransitionDuration: const Duration(seconds: 1),
transitionsBuilder: (context, animation, _, child) =>
FadeTransition(
opacity: animation,
child: child,
),
pageBuilder: (context, _, __) => ScreenTwo(index: index),
),
);
},
child: getCard(index),
);
}
Widget getCard(int index) {
return Stack(
children: [
Positioned.fill(
child: Hero(
createRectTween: (Rect? begin, Rect? end) {
return CurvedRectArcTween(begin: begin, end: end);
},
tag: 'hero_card_$index',
child: const Card(),
),
),
Positioned.fill(
child: Padding(
padding: const EdgeInsets.all(8),
child: Hero(
tag: 'hero_image_$index',
createRectTween: (Rect? begin, Rect? end) {
return CurvedRectArcTween(begin: begin, end: end);
},
child: Image.network(
'https://picsum.photos/200',
fit: BoxFit.contain,
alignment: Alignment.topCenter,
),
),
),
)
],
);
}
}
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 Material(
color: Colors.transparent,
child: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
height: 500,
width: 416,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: getContent(),
),
),
),
),
),
);
}
Widget getContent() {
return Stack(
children: [
Positioned.fill(
child: Hero(
tag: 'hero_card_${widget.index}',
createRectTween: (Rect? begin, Rect? end) {
return RectTween(begin: begin, end: end);
},
child: const Card(),
),
),
Positioned.fill(
child: Padding(
padding: const EdgeInsets.all(8),
child: Hero(
tag: 'hero_image_${widget.index}',
createRectTween: (Rect? begin, Rect? end) {
return RectTween(begin: begin, end: end);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Image.network(
'https://picsum.photos/200',
fit: BoxFit.contain,
alignment: Alignment.topCenter,
),
),
],
),
),
),
)
],
);
}
}
Результат