| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../painting/image_test_utils.dart' show TestImageProvider; |
| |
| Future<ui.Image> createTestImage() { |
| final ui.Paint paint = ui.Paint() |
| ..style = ui.PaintingStyle.stroke |
| ..strokeWidth = 1.0; |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas pictureCanvas = ui.Canvas(recorder); |
| pictureCanvas.drawCircle(Offset.zero, 20.0, paint); |
| final ui.Picture picture = recorder.endRecording(); |
| return picture.toImage(300, 300); |
| } |
| |
| Key firstKey = const Key('first'); |
| Key secondKey = const Key('second'); |
| Key thirdKey = const Key('third'); |
| Key simpleKey = const Key('simple'); |
| |
| Key homeRouteKey = const Key('homeRoute'); |
| Key routeTwoKey = const Key('routeTwo'); |
| Key routeThreeKey = const Key('routeThree'); |
| |
| bool transitionFromUserGestures = false; |
| |
| final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => Material( |
| child: ListView( |
| key: homeRouteKey, |
| children: <Widget>[ |
| const SizedBox(height: 100.0, width: 100.0), |
| Card(child: Hero( |
| tag: 'a', |
| transitionOnUserGestures: transitionFromUserGestures, |
| child: SizedBox(height: 100.0, width: 100.0, key: firstKey), |
| )), |
| const SizedBox(height: 100.0, width: 100.0), |
| TextButton( |
| child: const Text('two'), |
| onPressed: () { Navigator.pushNamed(context, '/two'); }, |
| ), |
| TextButton( |
| child: const Text('twoInset'), |
| onPressed: () { Navigator.pushNamed(context, '/twoInset'); }, |
| ), |
| TextButton( |
| child: const Text('simple'), |
| onPressed: () { Navigator.pushNamed(context, '/simple'); }, |
| ), |
| ], |
| ), |
| ), |
| '/two': (BuildContext context) => Material( |
| child: ListView( |
| key: routeTwoKey, |
| children: <Widget>[ |
| TextButton( |
| child: const Text('pop'), |
| onPressed: () { Navigator.pop(context); }, |
| ), |
| const SizedBox(height: 150.0, width: 150.0), |
| Card(child: Hero( |
| tag: 'a', |
| transitionOnUserGestures: transitionFromUserGestures, |
| child: SizedBox(height: 150.0, width: 150.0, key: secondKey), |
| )), |
| const SizedBox(height: 150.0, width: 150.0), |
| TextButton( |
| child: const Text('three'), |
| onPressed: () { Navigator.push(context, ThreeRoute()); }, |
| ), |
| ], |
| ), |
| ), |
| // This route is the same as /two except that Hero 'a' is shifted to the right by |
| // 50 pixels. When the hero's in-flight bounds between / and /twoInset are animated |
| // using MaterialRectArcTween (the default) they'll follow a different path |
| // then when the flight starts at /twoInset and returns to /. |
| '/twoInset': (BuildContext context) => Material( |
| child: ListView( |
| key: routeTwoKey, |
| children: <Widget>[ |
| TextButton( |
| child: const Text('pop'), |
| onPressed: () { Navigator.pop(context); }, |
| ), |
| const SizedBox(height: 150.0, width: 150.0), |
| Card( |
| child: Padding( |
| padding: const EdgeInsets.only(left: 50.0), |
| child: Hero( |
| tag: 'a', |
| transitionOnUserGestures: transitionFromUserGestures, |
| child: SizedBox(height: 150.0, width: 150.0, key: secondKey), |
| ), |
| ), |
| ), |
| const SizedBox(height: 150.0, width: 150.0), |
| TextButton( |
| child: const Text('three'), |
| onPressed: () { Navigator.push(context, ThreeRoute()); }, |
| ), |
| ], |
| ), |
| ), |
| // This route is the same as /two except that Hero 'a' is shifted to the right by |
| // 50 pixels. When the hero's in-flight bounds between / and /twoInset are animated |
| // using MaterialRectArcTween (the default) they'll follow a different path |
| // then when the flight starts at /twoInset and returns to /. |
| '/simple': (BuildContext context) => CupertinoPageScaffold( |
| child: Center( |
| child: Hero( |
| tag: 'a', |
| transitionOnUserGestures: transitionFromUserGestures, |
| child: SizedBox(height: 150.0, width: 150.0, key: simpleKey), |
| ), |
| ), |
| ), |
| }; |
| |
| class ThreeRoute extends MaterialPageRoute<void> { |
| ThreeRoute() |
| : super(builder: (BuildContext context) { |
| return Material( |
| key: routeThreeKey, |
| child: ListView( |
| children: <Widget>[ |
| const SizedBox(height: 200.0, width: 200.0), |
| Card(child: Hero(tag: 'a', child: SizedBox(height: 200.0, width: 200.0, key: thirdKey))), |
| const SizedBox(height: 200.0, width: 200.0), |
| ], |
| ), |
| ); |
| }); |
| } |
| |
| class MutatingRoute extends MaterialPageRoute<void> { |
| MutatingRoute() |
| : super(builder: (BuildContext context) { |
| return Hero(tag: 'a', key: UniqueKey(), child: const Text('MutatingRoute')); |
| }); |
| |
| void markNeedsBuild() { |
| setState(() { |
| // Trigger a rebuild |
| }); |
| } |
| } |
| |
| class _SimpleStatefulWidget extends StatefulWidget { |
| const _SimpleStatefulWidget({ Key? key }) : super(key: key); |
| @override |
| _SimpleState createState() => _SimpleState(); |
| } |
| |
| class _SimpleState extends State<_SimpleStatefulWidget> { |
| int state = 0; |
| |
| @override |
| Widget build(BuildContext context) => Text(state.toString()); |
| } |
| |
| class MyStatefulWidget extends StatefulWidget { |
| const MyStatefulWidget({ Key? key, this.value = '123' }) : super(key: key); |
| final String value; |
| @override |
| MyStatefulWidgetState createState() => MyStatefulWidgetState(); |
| } |
| |
| class MyStatefulWidgetState extends State<MyStatefulWidget> { |
| @override |
| Widget build(BuildContext context) => Text(widget.value); |
| } |
| |
| Future<void> main() async { |
| final ui.Image testImage = await createTestImage(); |
| assert(testImage != null); |
| |
| setUp(() { |
| transitionFromUserGestures = false; |
| }); |
| |
| testWidgets('Heroes animate', (WidgetTester tester) async { |
| |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| |
| // the initial setup. |
| |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| |
| await tester.tap(find.text('two')); |
| await tester.pump(); // begin navigation |
| |
| // at this stage, the second route is offstage, so that we can form the |
| // hero party. |
| |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey, skipOffstage: false), isOffstage); |
| expect(find.byKey(secondKey, skipOffstage: false), isInCard); |
| |
| await tester.pump(); |
| |
| // at this stage, the heroes have just gone on their journey, we are |
| // seeing them at t=16ms. The original page no longer contains the hero. |
| |
| expect(find.byKey(firstKey), findsNothing); |
| |
| expect(find.byKey(secondKey), findsOneWidget); |
| expect(find.byKey(secondKey), isNotInCard); |
| expect(find.byKey(secondKey), isOnstage); |
| |
| await tester.pump(); |
| |
| // t=32ms for the journey. Surely they are still at it. |
| |
| expect(find.byKey(firstKey), findsNothing); |
| |
| expect(find.byKey(secondKey), findsOneWidget); |
| |
| expect(find.byKey(secondKey), findsOneWidget); |
| expect(find.byKey(secondKey), isNotInCard); |
| expect(find.byKey(secondKey), isOnstage); |
| |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // t=1.032s for the journey. The journey has ended (it ends this frame, in |
| // fact). The hero should now be in the new page, onstage. The original |
| // widget will be back as well now (though not visible). |
| |
| expect(find.byKey(firstKey), findsNothing); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| |
| await tester.pump(); |
| |
| // Should not change anything. |
| |
| expect(find.byKey(firstKey), findsNothing); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| |
| // Now move on to view 3 |
| |
| await tester.tap(find.text('three')); |
| await tester.pump(); // begin navigation |
| |
| // at this stage, the second route is offstage, so that we can form the |
| // hero party. |
| |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| expect(find.byKey(thirdKey, skipOffstage: false), isOffstage); |
| expect(find.byKey(thirdKey, skipOffstage: false), isInCard); |
| |
| await tester.pump(); |
| |
| // at this stage, the heroes have just gone on their journey, we are |
| // seeing them at t=16ms. The original page no longer contains the hero. |
| |
| expect(find.byKey(secondKey), findsNothing); |
| expect(find.byKey(thirdKey), isOnstage); |
| expect(find.byKey(thirdKey), isNotInCard); |
| |
| await tester.pump(); |
| |
| // t=32ms for the journey. Surely they are still at it. |
| |
| expect(find.byKey(secondKey), findsNothing); |
| expect(find.byKey(thirdKey), isOnstage); |
| expect(find.byKey(thirdKey), isNotInCard); |
| |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // t=1.032s for the journey. The journey has ended (it ends this frame, in |
| // fact). The hero should now be in the new page, onstage. |
| |
| expect(find.byKey(secondKey), findsNothing); |
| expect(find.byKey(thirdKey), isOnstage); |
| expect(find.byKey(thirdKey), isInCard); |
| |
| await tester.pump(); |
| |
| // Should not change anything. |
| |
| expect(find.byKey(secondKey), findsNothing); |
| expect(find.byKey(thirdKey), isOnstage); |
| expect(find.byKey(thirdKey), isInCard); |
| }); |
| |
| testWidgets('Heroes still animate after hero controller is swapped.', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); |
| final UniqueKey heroKey = UniqueKey(); |
| await tester.pumpWidget( |
| HeroControllerScope( |
| controller: HeroController(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Navigator( |
| key: key, |
| initialRoute: 'navigator1', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return Hero( |
| tag: 'hero', |
| child: Container(), |
| flightShuttleBuilder: ( |
| BuildContext flightContext, |
| Animation<double> animation, |
| HeroFlightDirection flightDirection, |
| BuildContext fromHeroContext, |
| BuildContext toHeroContext, |
| ) { |
| return Container(key: heroKey); |
| }, |
| ); |
| }, |
| settings: s, |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| key.currentState!.push(MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return Hero( |
| tag: 'hero', |
| child: Container(), |
| flightShuttleBuilder: ( |
| BuildContext flightContext, |
| Animation<double> animation, |
| HeroFlightDirection flightDirection, |
| BuildContext fromHeroContext, |
| BuildContext toHeroContext, |
| ) { |
| return Container(key: heroKey); |
| }, |
| ); |
| }, |
| )); |
| expect(find.byKey(heroKey), findsNothing); |
| // Begins the navigation |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 30)); |
| expect(find.byKey(heroKey), isOnstage); |
| // Pumps a new hero controller. |
| await tester.pumpWidget( |
| HeroControllerScope( |
| controller: HeroController(), |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Navigator( |
| key: key, |
| initialRoute: 'navigator1', |
| onGenerateRoute: (RouteSettings s) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext c) { |
| return Hero( |
| tag: 'hero', |
| child: Container(), |
| flightShuttleBuilder: ( |
| BuildContext flightContext, |
| Animation<double> animation, |
| HeroFlightDirection flightDirection, |
| BuildContext fromHeroContext, |
| BuildContext toHeroContext, |
| ) { |
| return Container(key: heroKey); |
| }, |
| ); |
| }, |
| settings: s, |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| // The original animation still flies. |
| expect(find.byKey(heroKey), isOnstage); |
| // Waits for the animation finishes. |
| await tester.pumpAndSettle(); |
| expect(find.byKey(heroKey), findsNothing); |
| }); |
| |
| testWidgets('Heroes animate should hide original hero', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| // Checks initial state. |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| |
| await tester.tap(find.text('two')); |
| await tester.pumpAndSettle(); // Waits for transition finishes. |
| |
| expect(find.byKey(firstKey), findsNothing); |
| final Offstage first = tester.widget( |
| find.ancestor( |
| of: find.byKey(firstKey, skipOffstage: false), |
| matching: find.byType(Offstage, skipOffstage: false), |
| ).first, |
| ); |
| // Original hero should stay hidden. |
| expect(first.offstage, isTrue); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| }); |
| |
| testWidgets('Destination hero is rebuilt midflight', (WidgetTester tester) async { |
| final MutatingRoute route = MutatingRoute(); |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: ListView( |
| children: <Widget>[ |
| const Hero(tag: 'a', child: Text('foo')), |
| Builder(builder: (BuildContext context) { |
| return TextButton(child: const Text('two'), onPressed: () => Navigator.push(context, route)); |
| }), |
| ], |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('two')); |
| await tester.pump(const Duration(milliseconds: 10)); |
| |
| route.markNeedsBuild(); |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| await tester.pump(const Duration(seconds: 1)); |
| }); |
| |
| testWidgets('Heroes animation is fastOutSlowIn', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| await tester.tap(find.text('two')); |
| await tester.pump(); // begin navigation |
| |
| // Expect the height of the secondKey Hero to vary from 100 to 150 |
| // over duration and according to curve. |
| |
| const Duration duration = Duration(milliseconds: 300); |
| const Curve curve = Curves.fastOutSlowIn; |
| final double initialHeight = tester.getSize(find.byKey(firstKey, skipOffstage: false)).height; |
| final double finalHeight = tester.getSize(find.byKey(secondKey, skipOffstage: false)).height; |
| final double deltaHeight = finalHeight - initialHeight; |
| const double epsilon = 0.001; |
| |
| await tester.pump(duration * 0.25); |
| expect( |
| tester.getSize(find.byKey(secondKey)).height, |
| moreOrLessEquals(curve.transform(0.25) * deltaHeight + initialHeight, epsilon: epsilon), |
| ); |
| |
| await tester.pump(duration * 0.25); |
| expect( |
| tester.getSize(find.byKey(secondKey)).height, |
| moreOrLessEquals(curve.transform(0.50) * deltaHeight + initialHeight, epsilon: epsilon), |
| ); |
| |
| await tester.pump(duration * 0.25); |
| expect( |
| tester.getSize(find.byKey(secondKey)).height, |
| moreOrLessEquals(curve.transform(0.75) * deltaHeight + initialHeight, epsilon: epsilon), |
| ); |
| |
| await tester.pump(duration * 0.25); |
| expect( |
| tester.getSize(find.byKey(secondKey)).height, |
| moreOrLessEquals(curve.transform(1.0) * deltaHeight + initialHeight, epsilon: epsilon), |
| ); |
| }); |
| |
| testWidgets('Heroes are not interactive', (WidgetTester tester) async { |
| final List<String> log = <String>[]; |
| |
| await tester.pumpWidget(MaterialApp( |
| home: Center( |
| child: Hero( |
| tag: 'foo', |
| child: GestureDetector( |
| onTap: () { |
| log.add('foo'); |
| }, |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| child: Text('foo'), |
| ), |
| ), |
| ), |
| ), |
| routes: <String, WidgetBuilder>{ |
| '/next': (BuildContext context) { |
| return Align( |
| alignment: Alignment.topLeft, |
| child: Hero( |
| tag: 'foo', |
| child: GestureDetector( |
| onTap: () { |
| log.add('bar'); |
| }, |
| child: const SizedBox( |
| width: 100.0, |
| height: 150.0, |
| child: Text('bar'), |
| ), |
| ), |
| ), |
| ); |
| }, |
| }, |
| )); |
| |
| expect(log, isEmpty); |
| await tester.tap(find.text('foo')); |
| expect(log, equals(<String>['foo'])); |
| log.clear(); |
| |
| final NavigatorState navigator = tester.state(find.byType(Navigator)); |
| navigator.pushNamed('/next'); |
| |
| expect(log, isEmpty); |
| await tester.tap(find.text('foo', skipOffstage: false), warnIfMissed: false); |
| expect(log, isEmpty); |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| await tester.tap(find.text('foo', skipOffstage: false), warnIfMissed: false); |
| expect(log, isEmpty); |
| await tester.tap(find.text('bar', skipOffstage: false), warnIfMissed: false); |
| expect(log, isEmpty); |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(find.text('foo'), findsNothing); |
| await tester.tap(find.text('bar', skipOffstage: false), warnIfMissed: false); |
| expect(log, isEmpty); |
| |
| await tester.pump(const Duration(seconds: 1)); |
| expect(find.text('foo'), findsNothing); |
| await tester.tap(find.text('bar')); |
| expect(log, equals(<String>['bar'])); |
| }); |
| |
| testWidgets('Popping on first frame does not cause hero observer to crash', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| onGenerateRoute: (RouteSettings settings) { |
| return MaterialPageRoute<void>( |
| settings: settings, |
| builder: (BuildContext context) => Hero(tag: 'test', child: Container()), |
| ); |
| }, |
| )); |
| await tester.pump(); |
| |
| final Finder heroes = find.byType(Hero); |
| expect(heroes, findsOneWidget); |
| |
| Navigator.pushNamed(heroes.evaluate().first, 'test'); |
| await tester.pump(); // adds the new page to the tree... |
| |
| Navigator.pop(heroes.evaluate().first); |
| await tester.pump(); // ...and removes it straight away (since it's already at 0.0) |
| }); |
| |
| testWidgets('Overlapping starting and ending a hero transition works ok', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| onGenerateRoute: (RouteSettings settings) { |
| return MaterialPageRoute<void>( |
| settings: settings, |
| builder: (BuildContext context) => Hero(tag: 'test', child: Container()), |
| ); |
| }, |
| )); |
| await tester.pump(); |
| |
| final Finder heroes = find.byType(Hero); |
| expect(heroes, findsOneWidget); |
| |
| Navigator.pushNamed(heroes.evaluate().first, 'test'); |
| await tester.pump(); |
| await tester.pump(const Duration(hours: 1)); |
| |
| Navigator.pushNamed(heroes.evaluate().first, 'test'); |
| await tester.pump(); |
| await tester.pump(const Duration(hours: 1)); |
| |
| Navigator.pop(heroes.evaluate().first); |
| await tester.pump(); |
| Navigator.pop(heroes.evaluate().first); |
| await tester.pump(const Duration(hours: 1)); // so the first transition is finished, but the second hasn't started |
| await tester.pump(); |
| }); |
| |
| testWidgets('One route, two heroes, same tag, throws', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: ListView( |
| children: <Widget>[ |
| const Hero(tag: 'a', child: Text('a')), |
| const Hero(tag: 'a', child: Text('a too')), |
| Builder( |
| builder: (BuildContext context) { |
| return TextButton( |
| child: const Text('push'), |
| onPressed: () { |
| Navigator.push(context, PageRouteBuilder<void>( |
| pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { |
| return const Text('fail'); |
| }, |
| )); |
| }, |
| ); |
| }, |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('push')); |
| await tester.pump(); |
| final dynamic exception = tester.takeException(); |
| expect(exception, isFlutterError); |
| final FlutterError error = exception as FlutterError; |
| expect(error.diagnostics.length, 3); |
| final DiagnosticsNode last = error.diagnostics.last; |
| expect(last, isA<DiagnosticsProperty<StatefulElement>>()); |
| expect( |
| last.toStringDeep(), |
| equalsIgnoringHashCodes( |
| '# Here is the subtree for one of the offending heroes: Hero\n', |
| ), |
| ); |
| expect(last.style, DiagnosticsTreeStyle.dense); |
| expect( |
| error.toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'FlutterError\n' |
| ' There are multiple heroes that share the same tag within a\n' |
| ' subtree.\n' |
| ' Within each subtree for which heroes are to be animated (i.e. a\n' |
| ' PageRoute subtree), each Hero must have a unique non-null tag.\n' |
| ' In this case, multiple heroes had the following tag: a\n' |
| ' ├# Here is the subtree for one of the offending heroes: Hero\n', |
| ), |
| ); |
| }); |
| |
| testWidgets('Hero push transition interrupted by a pop', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| )); |
| |
| // Initially the firstKey Card on the '/' route is visible |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| |
| // Pushes MaterialPageRoute '/two'. |
| await tester.tap(find.text('two')); |
| |
| // Start the flight of Hero 'a' from route '/' to route '/two'. Route '/two' |
| // is now offstage. |
| await tester.pump(); |
| |
| final double initialHeight = tester.getSize(find.byKey(firstKey)).height; |
| final double finalHeight = tester.getSize(find.byKey(secondKey, skipOffstage: false)).height; |
| expect(finalHeight, greaterThan(initialHeight)); // simplify the checks below |
| |
| // Build the first hero animation frame in the navigator's overlay. |
| await tester.pump(); |
| |
| // At this point the hero widgets have been replaced by placeholders |
| // and the destination hero has been moved to the overlay. |
| expect(find.descendant(of: find.byKey(homeRouteKey), matching: find.byKey(firstKey)), findsNothing); |
| expect(find.descendant(of: find.byKey(routeTwoKey), matching: find.byKey(secondKey)), findsNothing); |
| expect(find.byKey(firstKey), findsNothing); |
| expect(find.byKey(secondKey), isOnstage); |
| |
| // The duration of a MaterialPageRoute's transition is 300ms. |
| // At 150ms Hero 'a' is mid-flight. |
| await tester.pump(const Duration(milliseconds: 150)); |
| final double height150ms = tester.getSize(find.byKey(secondKey)).height; |
| expect(height150ms, greaterThan(initialHeight)); |
| expect(height150ms, lessThan(finalHeight)); |
| |
| // Pop route '/two' before the push transition to '/two' has finished. |
| await tester.tap(find.text('pop')); |
| |
| // Restart the flight of Hero 'a'. Now it's flying from route '/two' to |
| // route '/'. |
| await tester.pump(); |
| |
| // After flying in the opposite direction for 50ms Hero 'a' will |
| // be smaller than it was, but bigger than its initial size. |
| await tester.pump(const Duration(milliseconds: 50)); |
| final double height100ms = tester.getSize(find.byKey(secondKey)).height; |
| expect(height100ms, lessThan(height150ms)); |
| expect(finalHeight, greaterThan(height100ms)); |
| |
| // Hero a's return flight at 149ms. The outgoing (push) flight took |
| // 150ms so we should be just about back to where Hero 'a' started. |
| const double epsilon = 0.001; |
| await tester.pump(const Duration(milliseconds: 99)); |
| moreOrLessEquals(tester.getSize(find.byKey(secondKey)).height - initialHeight, epsilon: epsilon); |
| |
| // The flight is finished. We're back to where we started. |
| await tester.pump(const Duration(milliseconds: 300)); |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| }); |
| |
| testWidgets('Hero pop transition interrupted by a push', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp(routes: routes), |
| ); |
| |
| // Pushes MaterialPageRoute '/two'. |
| await tester.tap(find.text('two')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // Now the secondKey Card on the '/2' route is visible |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| expect(find.byKey(firstKey), findsNothing); |
| |
| // Pop MaterialPageRoute '/two'. |
| await tester.tap(find.text('pop')); |
| |
| // Start the flight of Hero 'a' from route '/two' to route '/'. Route '/two' |
| // is now offstage. |
| await tester.pump(); |
| |
| final double initialHeight = tester.getSize(find.byKey(secondKey)).height; |
| final double finalHeight = tester.getSize(find.byKey(firstKey, skipOffstage: false)).height; |
| expect(finalHeight, lessThan(initialHeight)); // simplify the checks below |
| |
| // Build the first hero animation frame in the navigator's overlay. |
| await tester.pump(); |
| |
| // At this point the hero widgets have been replaced by placeholders |
| // and the destination hero has been moved to the overlay. |
| expect(find.descendant(of: find.byKey(homeRouteKey), matching: find.byKey(firstKey)), findsNothing); |
| expect(find.descendant(of: find.byKey(routeTwoKey), matching: find.byKey(secondKey)), findsNothing); |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(secondKey), findsNothing); |
| |
| // The duration of a MaterialPageRoute's transition is 300ms. |
| // At 150ms Hero 'a' is mid-flight. |
| await tester.pump(const Duration(milliseconds: 150)); |
| final double height150ms = tester.getSize(find.byKey(firstKey)).height; |
| expect(height150ms, lessThan(initialHeight)); |
| expect(height150ms, greaterThan(finalHeight)); |
| |
| // Push route '/two' before the pop transition from '/two' has finished. |
| await tester.tap(find.text('two')); |
| |
| // Restart the flight of Hero 'a'. Now it's flying from route '/' to |
| // route '/two'. |
| await tester.pump(); |
| |
| // After flying in the opposite direction for 50ms Hero 'a' will |
| // be smaller than it was, but bigger than its initial size. |
| await tester.pump(const Duration(milliseconds: 50)); |
| final double height200ms = tester.getSize(find.byKey(firstKey)).height; |
| expect(height200ms, greaterThan(height150ms)); |
| expect(finalHeight, lessThan(height200ms)); |
| |
| // Hero a's return flight at 149ms. The outgoing (push) flight took |
| // 150ms so we should be just about back to where Hero 'a' started. |
| const double epsilon = 0.001; |
| await tester.pump(const Duration(milliseconds: 99)); |
| moreOrLessEquals(tester.getSize(find.byKey(firstKey)).height - initialHeight, epsilon: epsilon); |
| |
| // The flight is finished. We're back to where we started. |
| await tester.pump(const Duration(milliseconds: 300)); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| expect(find.byKey(firstKey), findsNothing); |
| }); |
| |
| testWidgets('Destination hero disappears mid-flight', (WidgetTester tester) async { |
| const Key homeHeroKey = Key('home hero'); |
| const Key routeHeroKey = Key('route hero'); |
| bool routeIncludesHero = true; |
| late StateSetter heroCardSetState; |
| |
| // Show a 200x200 Hero tagged 'H', with key routeHeroKey |
| final MaterialPageRoute<void> route = MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Material( |
| child: ListView( |
| children: <Widget>[ |
| StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| heroCardSetState = setState; |
| return Card( |
| child: routeIncludesHero |
| ? const Hero(tag: 'H', child: SizedBox(key: routeHeroKey, height: 200.0, width: 200.0)) |
| : const SizedBox(height: 200.0, width: 200.0), |
| ); |
| }, |
| ), |
| TextButton( |
| child: const Text('POP'), |
| onPressed: () { Navigator.pop(context); }, |
| ), |
| ], |
| ), |
| ); |
| }, |
| ); |
| |
| // Show a 100x100 Hero tagged 'H' with key homeHeroKey |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { // Navigator.push() needs context |
| return ListView( |
| children: <Widget> [ |
| const Card( |
| child: Hero(tag: 'H', child: SizedBox(key: homeHeroKey, height: 100.0, width: 100.0)), |
| ), |
| TextButton( |
| child: const Text('PUSH'), |
| onPressed: () { Navigator.push(context, route); }, |
| ), |
| ], |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Pushes route |
| await tester.tap(find.text('PUSH')); |
| await tester.pump(); |
| await tester.pump(); |
| final double initialHeight = tester.getSize(find.byKey(routeHeroKey)).height; |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| double midflightHeight = tester.getSize(find.byKey(routeHeroKey)).height; |
| expect(midflightHeight, greaterThan(initialHeight)); |
| expect(midflightHeight, lessThan(200.0)); |
| |
| await tester.pump(const Duration(milliseconds: 300)); |
| await tester.pump(); |
| double finalHeight = tester.getSize(find.byKey(routeHeroKey)).height; |
| expect(finalHeight, 200.0); |
| |
| // Complete the flight |
| await tester.pump(const Duration(milliseconds: 100)); |
| |
| // Rebuild route with its Hero |
| |
| heroCardSetState(() { |
| routeIncludesHero = true; |
| }); |
| await tester.pump(); |
| |
| // Pops route |
| await tester.tap(find.text('POP')); |
| await tester.pump(); |
| await tester.pump(); |
| |
| await tester.pump(const Duration(milliseconds: 10)); |
| midflightHeight = tester.getSize(find.byKey(homeHeroKey)).height; |
| expect(midflightHeight, lessThan(finalHeight)); |
| expect(midflightHeight, greaterThan(100.0)); |
| |
| // Remove the destination hero midflight |
| heroCardSetState(() { |
| routeIncludesHero = false; |
| }); |
| await tester.pump(); |
| |
| await tester.pump(const Duration(milliseconds: 300)); |
| finalHeight = tester.getSize(find.byKey(homeHeroKey)).height; |
| expect(finalHeight, 100.0); |
| |
| }); |
| |
| testWidgets('Destination hero scrolls mid-flight', (WidgetTester tester) async { |
| const Key homeHeroKey = Key('home hero'); |
| const Key routeHeroKey = Key('route hero'); |
| const Key routeContainerKey = Key('route hero container'); |
| |
| // Show a 200x200 Hero tagged 'H', with key routeHeroKey |
| final MaterialPageRoute<void> route = MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Material( |
| child: ListView( |
| children: <Widget>[ |
| const SizedBox(height: 100.0), |
| // This container will appear at Y=100 |
| Container( |
| key: routeContainerKey, |
| child: const Hero(tag: 'H', child: SizedBox(key: routeHeroKey, height: 200.0, width: 200.0)), |
| ), |
| TextButton( |
| child: const Text('POP'), |
| onPressed: () { Navigator.pop(context); }, |
| ), |
| const SizedBox(height: 600.0), |
| ], |
| ), |
| ); |
| }, |
| ); |
| |
| // Show a 100x100 Hero tagged 'H' with key homeHeroKey |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData( |
| pageTransitionsTheme: const PageTransitionsTheme( |
| builders: <TargetPlatform, PageTransitionsBuilder>{ |
| TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), |
| }, |
| ), |
| ), |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { // Navigator.push() needs context |
| return ListView( |
| children: <Widget> [ |
| const SizedBox(height: 200.0), |
| // This container will appear at Y=200 |
| const Hero(tag: 'H', child: SizedBox(key: homeHeroKey, height: 100.0, width: 100.0)), |
| TextButton( |
| child: const Text('PUSH'), |
| onPressed: () { Navigator.push(context, route); }, |
| ), |
| const SizedBox(height: 600.0), |
| ], |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Pushes route |
| await tester.tap(find.text('PUSH')); |
| await tester.pump(); |
| await tester.pump(); |
| |
| final double initialY = tester.getTopLeft(find.byKey(routeHeroKey)).dy; |
| expect(initialY, 200.0); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| final double yAt100ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy; |
| expect(yAt100ms, lessThan(200.0)); |
| expect(yAt100ms, greaterThan(100.0)); |
| |
| // Scroll the target upwards by 25 pixels. The Hero flight's Y coordinate |
| // will be redirected from 100 to 75. |
| await tester.drag(find.byKey(routeContainerKey), const Offset(0.0, -25.0), warnIfMissed: false); // the container itself wouldn't be hit |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 10)); |
| final double yAt110ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy; |
| expect(yAt110ms, lessThan(yAt100ms)); |
| expect(yAt110ms, greaterThan(75.0)); |
| |
| await tester.pump(const Duration(milliseconds: 300)); |
| await tester.pump(); |
| final double finalHeroY = tester.getTopLeft(find.byKey(routeHeroKey)).dy; |
| expect(finalHeroY, 75.0); // 100 less 25 for the scroll |
| }); |
| |
| testWidgets('Destination hero scrolls out of view mid-flight', (WidgetTester tester) async { |
| const Key homeHeroKey = Key('home hero'); |
| const Key routeHeroKey = Key('route hero'); |
| const Key routeContainerKey = Key('route hero container'); |
| |
| // Show a 200x200 Hero tagged 'H', with key routeHeroKey |
| final MaterialPageRoute<void> route = MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Material( |
| child: ListView( |
| cacheExtent: 0.0, |
| children: <Widget>[ |
| const SizedBox(height: 100.0), |
| // This container will appear at Y=100 |
| Container( |
| key: routeContainerKey, |
| child: const Hero(tag: 'H', child: SizedBox(key: routeHeroKey, height: 200.0, width: 200.0)), |
| ), |
| const SizedBox(height: 800.0), |
| ], |
| ), |
| ); |
| }, |
| ); |
| |
| // Show a 100x100 Hero tagged 'H' with key homeHeroKey |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { // Navigator.push() needs context |
| return ListView( |
| children: <Widget> [ |
| const SizedBox(height: 200.0), |
| // This container will appear at Y=200 |
| const Hero(tag: 'H', child: SizedBox(key: homeHeroKey, height: 100.0, width: 100.0)), |
| TextButton( |
| child: const Text('PUSH'), |
| onPressed: () { Navigator.push(context, route); }, |
| ), |
| ], |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Pushes route |
| await tester.tap(find.text('PUSH')); |
| await tester.pump(); |
| await tester.pump(); |
| |
| final double initialY = tester.getTopLeft(find.byKey(routeHeroKey)).dy; |
| expect(initialY, 200.0); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| final double yAt100ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy; |
| expect(yAt100ms, lessThan(200.0)); |
| expect(yAt100ms, greaterThan(100.0)); |
| |
| await tester.drag(find.byKey(routeContainerKey), const Offset(0.0, -400.0), warnIfMissed: false); // the container itself wouldn't be hit |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(find.byKey(routeContainerKey), findsNothing); // Scrolled off the top |
| |
| // Flight continues (the hero will fade out) even though the destination |
| // no longer exists. |
| final double yAt110ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy; |
| expect(yAt110ms, lessThan(yAt100ms)); |
| expect(yAt110ms, greaterThan(100.0)); |
| |
| await tester.pump(const Duration(milliseconds: 300)); |
| await tester.pump(); |
| expect(find.byKey(routeHeroKey), findsNothing); |
| }); |
| |
| testWidgets('Aborted flight', (WidgetTester tester) async { |
| // See https://github.com/flutter/flutter/issues/5798 |
| const Key heroABKey = Key('AB hero'); |
| const Key heroBCKey = Key('BC hero'); |
| |
| // Show a 150x150 Hero tagged 'BC' |
| final MaterialPageRoute<void> routeC = MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Material( |
| child: ListView( |
| children: const <Widget>[ |
| // This container will appear at Y=0 |
| Hero( |
| tag: 'BC', |
| child: SizedBox( |
| key: heroBCKey, |
| height: 150.0, |
| child: Text('Hero'), |
| ), |
| ), |
| SizedBox(height: 800.0), |
| ], |
| ), |
| ); |
| }, |
| ); |
| |
| // Show a height=200 Hero tagged 'AB' and a height=50 Hero tagged 'BC' |
| final MaterialPageRoute<void> routeB = MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Material( |
| child: ListView( |
| children: <Widget>[ |
| const SizedBox(height: 100.0), |
| // This container will appear at Y=100 |
| const Hero( |
| tag: 'AB', |
| child: SizedBox( |
| key: heroABKey, |
| height: 200.0, |
| child: Text('Hero'), |
| ), |
| ), |
| TextButton( |
| child: const Text('PUSH C'), |
| onPressed: () { Navigator.push(context, routeC); }, |
| ), |
| const Hero( |
| tag: 'BC', |
| child: SizedBox( |
| height: 150.0, |
| child: Text('Hero'), |
| ), |
| ), |
| const SizedBox(height: 800.0), |
| ], |
| ), |
| ); |
| }, |
| ); |
| |
| // Show a 100x100 Hero tagged 'AB' with key heroABKey |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { // Navigator.push() needs context |
| return ListView( |
| children: <Widget> [ |
| const SizedBox(height: 200.0), |
| // This container will appear at Y=200 |
| const Hero( |
| tag: 'AB', |
| child: SizedBox( |
| height: 100.0, |
| width: 100.0, |
| child: Text('Hero'), |
| ), |
| ), |
| TextButton( |
| child: const Text('PUSH B'), |
| onPressed: () { Navigator.push(context, routeB); }, |
| ), |
| ], |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Pushes routeB |
| await tester.tap(find.text('PUSH B')); |
| await tester.pump(); |
| await tester.pump(); |
| |
| final double initialY = tester.getTopLeft(find.byKey(heroABKey)).dy; |
| expect(initialY, 200.0); |
| |
| await tester.pump(const Duration(milliseconds: 200)); |
| final double yAt200ms = tester.getTopLeft(find.byKey(heroABKey)).dy; |
| // Hero AB is mid flight. |
| expect(yAt200ms, lessThan(200.0)); |
| expect(yAt200ms, greaterThan(100.0)); |
| |
| // Pushes route C, causes hero AB's flight to abort, hero BC's flight to start |
| await tester.tap(find.text('PUSH C')); |
| await tester.pump(); |
| await tester.pump(); |
| |
| // Hero AB's aborted flight finishes where it was expected although |
| // it's been faded out. |
| await tester.pump(const Duration(milliseconds: 100)); |
| expect(tester.getTopLeft(find.byKey(heroABKey)).dy, 100.0); |
| |
| bool _isVisible(Element node) { |
| bool isVisible = true; |
| node.visitAncestorElements((Element ancestor) { |
| final RenderObject r = ancestor.renderObject!; |
| if (r is RenderOpacity && r.opacity == 0) { |
| isVisible = false; |
| return false; |
| } |
| return true; |
| }); |
| return isVisible; |
| } |
| |
| // Of all heroes only one should be visible now. |
| final Iterable<Element> elements = find.text('Hero').evaluate(); |
| expect(elements.where(_isVisible).length, 1); |
| |
| // Hero BC's flight finishes normally. |
| await tester.pump(const Duration(milliseconds: 300)); |
| expect(tester.getTopLeft(find.byKey(heroBCKey)).dy, 0.0); |
| }); |
| |
| testWidgets('Stateful hero child state survives flight', (WidgetTester tester) async { |
| final MaterialPageRoute<void> route = MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Material( |
| child: ListView( |
| children: <Widget>[ |
| const Card( |
| child: Hero( |
| tag: 'H', |
| child: SizedBox( |
| height: 200.0, |
| child: MyStatefulWidget(value: '456'), |
| ), |
| ), |
| ), |
| TextButton( |
| child: const Text('POP'), |
| onPressed: () { Navigator.pop(context); }, |
| ), |
| ], |
| ), |
| ); |
| }, |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Builder( |
| builder: (BuildContext context) { // Navigator.push() needs context |
| return ListView( |
| children: <Widget> [ |
| const Card( |
| child: Hero( |
| tag: 'H', |
| child: SizedBox( |
| height: 100.0, |
| child: MyStatefulWidget(value: '456'), |
| ), |
| ), |
| ), |
| TextButton( |
| child: const Text('PUSH'), |
| onPressed: () { Navigator.push(context, route); }, |
| ), |
| ], |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('456'), findsOneWidget); |
| |
| // Push route. |
| await tester.tap(find.text('PUSH')); |
| await tester.pump(); |
| await tester.pump(); |
| |
| // Push flight underway. |
| await tester.pump(const Duration(milliseconds: 100)); |
| // Visible in the hero animation. |
| expect(find.text('456'), findsOneWidget); |
| |
| // Push flight finished. |
| await tester.pump(const Duration(milliseconds: 300)); |
| expect(find.text('456'), findsOneWidget); |
| |
| // Pop route. |
| await tester.tap(find.text('POP')); |
| await tester.pump(); |
| await tester.pump(); |
| |
| // Pop flight underway. |
| await tester.pump(const Duration(milliseconds: 100)); |
| expect(find.text('456'), findsOneWidget); |
| |
| // Pop flight finished |
| await tester.pump(const Duration(milliseconds: 300)); |
| expect(find.text('456'), findsOneWidget); |
| |
| }); |
| |
| testWidgets('Hero createRectTween', (WidgetTester tester) async { |
| RectTween createRectTween(Rect? begin, Rect? end) { |
| return MaterialRectCenterArcTween(begin: begin, end: end); |
| } |
| |
| final Map<String, WidgetBuilder> createRectTweenHeroRoutes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => Material( |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.start, |
| children: <Widget>[ |
| Hero( |
| tag: 'a', |
| createRectTween: createRectTween, |
| child: SizedBox(height: 100.0, width: 100.0, key: firstKey), |
| ), |
| TextButton( |
| child: const Text('two'), |
| onPressed: () { Navigator.pushNamed(context, '/two'); }, |
| ), |
| ], |
| ), |
| ), |
| '/two': (BuildContext context) => Material( |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.center, |
| children: <Widget>[ |
| SizedBox( |
| height: 200.0, |
| child: TextButton( |
| child: const Text('pop'), |
| onPressed: () { Navigator.pop(context); }, |
| ), |
| ), |
| Hero( |
| tag: 'a', |
| createRectTween: createRectTween, |
| child: SizedBox(height: 200.0, width: 100.0, key: secondKey), |
| ), |
| ], |
| ), |
| ), |
| }; |
| |
| await tester.pumpWidget(MaterialApp(routes: createRectTweenHeroRoutes)); |
| expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0)); |
| |
| const double epsilon = 0.001; |
| const Duration duration = Duration(milliseconds: 300); |
| const Curve curve = Curves.fastOutSlowIn; |
| final MaterialPointArcTween pushCenterTween = MaterialPointArcTween( |
| begin: const Offset(50.0, 50.0), |
| end: const Offset(400.0, 300.0), |
| ); |
| |
| await tester.tap(find.text('two')); |
| await tester.pump(); // begin navigation |
| |
| // Verify that the center of the secondKey Hero flies along the |
| // pushCenterTween arc for the push /two flight. |
| |
| await tester.pump(); |
| expect(tester.getCenter(find.byKey(secondKey)), const Offset(50.0, 50.0)); |
| |
| await tester.pump(duration * 0.25); |
| Offset actualHeroCenter = tester.getCenter(find.byKey(secondKey)); |
| Offset predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.25)); |
| expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter)); |
| |
| await tester.pump(duration * 0.25); |
| actualHeroCenter = tester.getCenter(find.byKey(secondKey)); |
| predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.5)); |
| expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter)); |
| |
| await tester.pump(duration * 0.25); |
| actualHeroCenter = tester.getCenter(find.byKey(secondKey)); |
| predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.75)); |
| expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter)); |
| |
| await tester.pumpAndSettle(); |
| expect(tester.getCenter(find.byKey(secondKey)), const Offset(400.0, 300.0)); |
| |
| // Verify that the center of the firstKey Hero flies along the |
| // pushCenterTween arc for the pop /two flight. |
| |
| await tester.tap(find.text('pop')); |
| await tester.pump(); // begin navigation |
| |
| final MaterialPointArcTween popCenterTween = MaterialPointArcTween( |
| begin: const Offset(400.0, 300.0), |
| end: const Offset(50.0, 50.0), |
| ); |
| await tester.pump(); |
| expect(tester.getCenter(find.byKey(firstKey)), const Offset(400.0, 300.0)); |
| |
| await tester.pump(duration * 0.25); |
| actualHeroCenter = tester.getCenter(find.byKey(firstKey)); |
| predictedHeroCenter = popCenterTween.lerp(curve.transform(0.25)); |
| expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter)); |
| |
| await tester.pump(duration * 0.25); |
| actualHeroCenter = tester.getCenter(find.byKey(firstKey)); |
| predictedHeroCenter = popCenterTween.lerp(curve.transform(0.5)); |
| expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter)); |
| |
| await tester.pump(duration * 0.25); |
| actualHeroCenter = tester.getCenter(find.byKey(firstKey)); |
| predictedHeroCenter = popCenterTween.lerp(curve.transform(0.75)); |
| expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter)); |
| |
| await tester.pumpAndSettle(); |
| expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0)); |
| }); |
| |
| testWidgets('Hero createRectTween for Navigator that is not full screen', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/25272 |
| |
| RectTween createRectTween(Rect? begin, Rect? end) { |
| return RectTween(begin: begin, end: end); |
| } |
| |
| final Map<String, WidgetBuilder> createRectTweenHeroRoutes = <String, WidgetBuilder>{ |
| '/': (BuildContext context) => Material( |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.start, |
| children: <Widget>[ |
| Hero( |
| tag: 'a', |
| createRectTween: createRectTween, |
| child: SizedBox(height: 100.0, width: 100.0, key: firstKey), |
| ), |
| TextButton( |
| child: const Text('two'), |
| onPressed: () { Navigator.pushNamed(context, '/two'); }, |
| ), |
| ], |
| ), |
| ), |
| '/two': (BuildContext context) => Material( |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.center, |
| children: <Widget>[ |
| SizedBox( |
| height: 200.0, |
| child: TextButton( |
| child: const Text('pop'), |
| onPressed: () { Navigator.pop(context); }, |
| ), |
| ), |
| Hero( |
| tag: 'a', |
| createRectTween: createRectTween, |
| child: SizedBox(height: 200.0, width: 100.0, key: secondKey), |
| ), |
| ], |
| ), |
| ), |
| }; |
| |
| const double leftPadding = 10.0; |
| |
| // MaterialApp and its Navigator are offset from the left |
| await tester.pumpWidget(Padding( |
| padding: const EdgeInsets.only(left: leftPadding), |
| child: MaterialApp(routes: createRectTweenHeroRoutes), |
| )); |
| expect(tester.getCenter(find.byKey(firstKey)), const Offset(leftPadding + 50.0, 50.0)); |
| |
| const double epsilon = 0.001; |
| const Duration duration = Duration(milliseconds: 300); |
| const Curve curve = Curves.fastOutSlowIn; |
| final RectTween pushRectTween = RectTween( |
| begin: const Rect.fromLTWH(leftPadding, 0.0, 100.0, 100.0), |
| end: const Rect.fromLTWH(350.0 + leftPadding / 2, 200.0, 100.0, 200.0), |
| ); |
| |
| await tester.tap(find.text('two')); |
| await tester.pump(); // begin navigation |
| |
| // Verify that the rect of the secondKey Hero transforms as the |
| // pushRectTween rect for the push /two flight. |
| |
| await tester.pump(); |
| expect(tester.getCenter(find.byKey(secondKey)), const Offset(50.0 + leftPadding, 50.0)); |
| |
| await tester.pump(duration * 0.25); |
| Rect actualHeroRect = tester.getRect(find.byKey(secondKey)); |
| Rect predictedHeroRect = pushRectTween.lerp(curve.transform(0.25))!; |
| expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); |
| |
| await tester.pump(duration * 0.25); |
| actualHeroRect = tester.getRect(find.byKey(secondKey)); |
| predictedHeroRect = pushRectTween.lerp(curve.transform(0.5))!; |
| expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); |
| |
| await tester.pump(duration * 0.25); |
| actualHeroRect = tester.getRect(find.byKey(secondKey)); |
| predictedHeroRect = pushRectTween.lerp(curve.transform(0.75))!; |
| expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); |
| |
| await tester.pumpAndSettle(); |
| expect(tester.getCenter(find.byKey(secondKey)), const Offset(400.0 + leftPadding / 2, 300.0)); |
| |
| // Verify that the rect of the firstKey Hero transforms as the |
| // pushRectTween rect for the pop /two flight. |
| |
| await tester.tap(find.text('pop')); |
| await tester.pump(); // begin navigation |
| |
| final RectTween popRectTween = RectTween( |
| begin: const Rect.fromLTWH(350.0 + leftPadding / 2, 200.0, 100.0, 200.0), |
| end: const Rect.fromLTWH(leftPadding, 0.0, 100.0, 100.0), |
| ); |
| await tester.pump(); |
| expect(tester.getCenter(find.byKey(firstKey)), const Offset(400.0 + leftPadding / 2, 300.0)); |
| |
| await tester.pump(duration * 0.25); |
| actualHeroRect = tester.getRect(find.byKey(firstKey)); |
| predictedHeroRect = popRectTween.lerp(curve.transform(0.25))!; |
| expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); |
| |
| await tester.pump(duration * 0.25); |
| actualHeroRect = tester.getRect(find.byKey(firstKey)); |
| predictedHeroRect = popRectTween.lerp(curve.transform(0.5))!; |
| expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); |
| |
| await tester.pump(duration * 0.25); |
| actualHeroRect = tester.getRect(find.byKey(firstKey)); |
| predictedHeroRect = popRectTween.lerp(curve.transform(0.75))!; |
| expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect)); |
| |
| await tester.pumpAndSettle(); |
| expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0 + leftPadding, 50.0)); |
| }); |
| |
| |
| testWidgets('Pop interrupts push, reverses flight', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp(routes: routes)); |
| await tester.tap(find.text('twoInset')); |
| await tester.pump(); // begin navigation from / to /twoInset. |
| |
| const double epsilon = 0.001; |
| const Duration duration = Duration(milliseconds: 300); |
| |
| await tester.pump(); |
| final double x0 = tester.getTopLeft(find.byKey(secondKey)).dx; |
| |
| // Flight begins with the secondKey Hero widget lined up with the firstKey widget. |
| expect(x0, 4.0); |
| |
| await tester.pump(duration * 0.1); |
| final double x1 = tester.getTopLeft(find.byKey(secondKey)).dx; |
| |
| await tester.pump(duration * 0.1); |
| final double x2 = tester.getTopLeft(find.byKey(secondKey)).dx; |
| |
| await tester.pump(duration * 0.1); |
| final double x3 = tester.getTopLeft(find.byKey(secondKey)).dx; |
| |
| await tester.pump(duration * 0.1); |
| final double x4 = tester.getTopLeft(find.byKey(secondKey)).dx; |
| |
| // Pop route /twoInset before the push transition from / to /twoInset has finished. |
| await tester.tap(find.text('pop')); |
| |
| |
| // We expect the hero to take the same path as it did flying from / |
| // to /twoInset as it does now, flying from '/twoInset' back to /. The most |
| // important checks below are the first (x4) and last (x0): the hero should |
| // not jump from where it was when the push transition was interrupted by a |
| // pop, and it should end up where the push started. |
| |
| await tester.pump(); |
| expect(tester.getTopLeft(find.byKey(secondKey)).dx, moreOrLessEquals(x4, epsilon: epsilon)); |
| |
| await tester.pump(duration * 0.1); |
| expect(tester.getTopLeft(find.byKey(secondKey)).dx, moreOrLessEquals(x3, epsilon: epsilon)); |
| |
| await tester.pump(duration * 0.1); |
| expect(tester.getTopLeft(find.byKey(secondKey)).dx, moreOrLessEquals(x2, epsilon: epsilon)); |
| |
| await tester.pump(duration * 0.1); |
| expect(tester.getTopLeft(find.byKey(secondKey)).dx, moreOrLessEquals(x1, epsilon: epsilon)); |
| |
| await tester.pump(duration * 0.1); |
| expect(tester.getTopLeft(find.byKey(secondKey)).dx, moreOrLessEquals(x0, epsilon: epsilon)); |
| |
| // Below: show that a different pop Hero path is in fact taken after |
| // a completed push transition. |
| |
| // Complete the pop transition and we're back to showing /. |
| await tester.pumpAndSettle(); |
| expect(tester.getTopLeft(find.byKey(firstKey)).dx, 4.0); // Card contents are inset by 4.0. |
| |
| // Push /twoInset and wait for the transition to finish. |
| await tester.tap(find.text('twoInset')); |
| await tester.pumpAndSettle(); |
| expect(tester.getTopLeft(find.byKey(secondKey)).dx, 54.0); |
| |
| // Start the pop transition from /twoInset to /. |
| await tester.tap(find.text('pop')); |
| await tester.pump(); |
| |
| // Now the firstKey widget is the flying hero widget and it starts |
| // out lined up with the secondKey widget. |
| await tester.pump(); |
| expect(tester.getTopLeft(find.byKey(firstKey)).dx, 54.0); |
| |
| // x0-x4 are the top left x coordinates for the beginning 40% of |
| // the incoming flight. Advance the outgoing flight to the same |
| // place. |
| await tester.pump(duration * 0.6); |
| |
| await tester.pump(duration * 0.1); |
| expect(tester.getTopLeft(find.byKey(firstKey)).dx, isNot(moreOrLessEquals(x4, epsilon: epsilon))); |
| |
| await tester.pump(duration * 0.1); |
| expect(tester.getTopLeft(find.byKey(firstKey)).dx, isNot(moreOrLessEquals(x3, epsilon: epsilon))); |
| |
| // At this point the flight path arcs do start to get pretty close so |
| // there's no point in comparing them. |
| await tester.pump(duration * 0.1); |
| |
| // After the remaining 40% of the incoming flight is complete, we |
| // expect to end up where the outgoing flight started. |
| await tester.pump(duration * 0.1); |
| expect(tester.getTopLeft(find.byKey(firstKey)).dx, x0); |
| }); |
| |
| testWidgets('Can override flight shuttle in to hero', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: ListView( |
| children: <Widget>[ |
| const Hero(tag: 'a', child: Text('foo')), |
| Builder(builder: (BuildContext context) { |
| return TextButton( |
| child: const Text('two'), |
| onPressed: () => Navigator.push<void>(context, MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Material( |
| child: Hero( |
| tag: 'a', |
| child: const Text('bar'), |
| flightShuttleBuilder: ( |
| BuildContext flightContext, |
| Animation<double> animation, |
| HeroFlightDirection flightDirection, |
| BuildContext fromHeroContext, |
| BuildContext toHeroContext, |
| ) { |
| return const Text('baz'); |
| }, |
| ), |
| ); |
| }, |
| )), |
| ); |
| }), |
| ], |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('two')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 10)); |
| |
| expect(find.text('foo'), findsNothing); |
| expect(find.text('bar'), findsNothing); |
| expect(find.text('baz'), findsOneWidget); |
| }); |
| |
| testWidgets('Can override flight shuttle in from hero', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: ListView( |
| children: <Widget>[ |
| Hero( |
| tag: 'a', |
| child: const Text('foo'), |
| flightShuttleBuilder: ( |
| BuildContext flightContext, |
| Animation<double> animation, |
| HeroFlightDirection flightDirection, |
| BuildContext fromHeroContext, |
| BuildContext toHeroContext, |
| ) { return const Text('baz'); }, |
| ), |
| Builder(builder: (BuildContext context) { |
| return TextButton( |
| child: const Text('two'), |
| onPressed: () => Navigator.push<void>(context, MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return const Material( |
| child: Hero(tag: 'a', child: Text('bar')), |
| ); |
| }, |
| )), |
| ); |
| }), |
| ], |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('two')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 10)); |
| |
| expect(find.text('foo'), findsNothing); |
| expect(find.text('bar'), findsNothing); |
| expect(find.text('baz'), findsOneWidget); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/77720. |
| testWidgets("toHero's shuttle builder over fromHero's shuttle builder", (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: ListView( |
| children: <Widget>[ |
| Hero( |
| tag: 'a', |
| child: const Text('foo'), |
| flightShuttleBuilder: ( |
| BuildContext flightContext, |
| Animation<double> animation, |
| HeroFlightDirection flightDirection, |
| BuildContext fromHeroContext, |
| BuildContext toHeroContext, |
| ) { return const Text('fromHero text'); }, |
| ), |
| Builder(builder: (BuildContext context) { |
| return TextButton( |
| child: const Text('two'), |
| onPressed: () => Navigator.push<void>(context, MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Material( |
| child: Hero( |
| tag: 'a', |
| child: const Text('bar'), |
| flightShuttleBuilder: ( |
| BuildContext flightContext, |
| Animation<double> animation, |
| HeroFlightDirection flightDirection, |
| BuildContext fromHeroContext, |
| BuildContext toHeroContext, |
| ) { return const Text('toHero text'); }, |
| ), |
| ); |
| }, |
| )), |
| ); |
| }), |
| ], |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('two')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 10)); |
| |
| expect(find.text('foo'), findsNothing); |
| expect(find.text('bar'), findsNothing); |
| expect(find.text('fromHero text'), findsNothing); |
| expect(find.text('toHero text'), findsOneWidget); |
| }); |
| |
| testWidgets('Can override flight launch pads', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: ListView( |
| children: <Widget>[ |
| Hero( |
| tag: 'a', |
| child: const Text('Batman'), |
| placeholderBuilder: (BuildContext context, Size heroSize, Widget child) { |
| return const Text('Venom'); |
| }, |
| ), |
| Builder(builder: (BuildContext context) { |
| return TextButton( |
| child: const Text('two'), |
| onPressed: () => Navigator.push<void>(context, MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Material( |
| child: Hero( |
| tag: 'a', |
| child: const Text('Wolverine'), |
| placeholderBuilder: (BuildContext context, Size size, Widget child) { |
| return const Text('Joker'); |
| }, |
| ), |
| ); |
| }, |
| )), |
| ); |
| }), |
| ], |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('two')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 10)); |
| |
| expect(find.text('Batman'), findsNothing); |
| // This shows up once but in the Hero because by default, the destination |
| // Hero child is the widget in flight. |
| expect(find.text('Wolverine'), findsOneWidget); |
| expect(find.text('Venom'), findsOneWidget); |
| expect(find.text('Joker'), findsOneWidget); |
| }); |
| |
| testWidgets('Heroes do not transition on back gestures by default', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| )); |
| |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| |
| await tester.tap(find.text('two')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(find.byKey(firstKey), findsNothing); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0)); |
| await gesture.moveBy(const Offset(20.0, 0.0)); |
| await gesture.moveBy(const Offset(180.0, 0.0)); |
| await gesture.up(); |
| await tester.pump(); |
| |
| await tester.pump(); |
| |
| // Both Heroes exist and are seated in their normal parents. |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| |
| // To make sure the hero had all chances of starting. |
| await tester.pump(const Duration(milliseconds: 100)); |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('Heroes can transition on gesture in one frame', (WidgetTester tester) async { |
| transitionFromUserGestures = true; |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| )); |
| |
| await tester.tap(find.text('two')); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(find.byKey(firstKey), findsNothing); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0)); |
| await gesture.moveBy(const Offset(200.0, 0.0)); |
| await tester.pump(); |
| |
| // We're going to page 1 so page 1's Hero is lifted into flight. |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isNotInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| |
| // Move further along. |
| await gesture.moveBy(const Offset(500.0, 0.0)); |
| await tester.pump(); |
| |
| // Same results. |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isNotInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| |
| await gesture.up(); |
| // Finish transition. |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| // Hero A is back in the card. |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('Heroes animate should hide destination hero and display original hero in case of dismissed', (WidgetTester tester) async { |
| transitionFromUserGestures = true; |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| )); |
| |
| await tester.tap(find.text('two')); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byKey(firstKey), findsNothing); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0)); |
| await gesture.moveBy(const Offset(50.0, 0.0)); |
| await tester.pump(); |
| // It will only register the drag if we move a second time. |
| await gesture.moveBy(const Offset(50.0, 0.0)); |
| await tester.pump(); |
| |
| // We're going to page 1 so page 1's Hero is lifted into flight. |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isNotInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| |
| // Dismisses hero transition. |
| await gesture.up(); |
| await tester.pump(); |
| await tester.pumpAndSettle(); |
| |
| // We goes back to second page. |
| expect(find.byKey(firstKey), findsNothing); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('Handles transitions when a non-default initial route is set', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| initialRoute: '/two', |
| )); |
| expect(tester.takeException(), isNull); |
| expect(find.text('two'), findsNothing); |
| expect(find.text('three'), findsOneWidget); |
| }); |
| |
| testWidgets('Can push/pop on outer Navigator if nested Navigator contains Heroes', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/28042. |
| |
| const String heroTag = 'You are my hero!'; |
| final GlobalKey<NavigatorState> rootNavigator = GlobalKey(); |
| final GlobalKey<NavigatorState> nestedNavigator = GlobalKey(); |
| final Key nestedRouteHeroBottom = UniqueKey(); |
| final Key nestedRouteHeroTop = UniqueKey(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: rootNavigator, |
| home: Navigator( |
| key: nestedNavigator, |
| onGenerateRoute: (RouteSettings settings) { |
| return MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Hero( |
| tag: heroTag, |
| child: Placeholder( |
| key: nestedRouteHeroBottom, |
| ), |
| ); |
| }, |
| ); |
| }, |
| ), |
| ), |
| ); |
| |
| nestedNavigator.currentState!.push(MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Hero( |
| tag: heroTag, |
| child: Placeholder( |
| key: nestedRouteHeroTop, |
| ), |
| ); |
| }, |
| )); |
| await tester.pumpAndSettle(); |
| |
| // Both heroes are in the tree, one is offstage |
| expect(find.byKey(nestedRouteHeroTop), findsOneWidget); |
| expect(find.byKey(nestedRouteHeroBottom), findsNothing); |
| expect(find.byKey(nestedRouteHeroBottom, skipOffstage: false), findsOneWidget); |
| |
| rootNavigator.currentState!.push(MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return const Text('Foo'); |
| }, |
| )); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Foo'), findsOneWidget); |
| // Both heroes are still in the tree, both are offstage. |
| expect(find.byKey(nestedRouteHeroBottom), findsNothing); |
| expect(find.byKey(nestedRouteHeroTop), findsNothing); |
| expect(find.byKey(nestedRouteHeroBottom, skipOffstage: false), findsOneWidget); |
| expect(find.byKey(nestedRouteHeroTop, skipOffstage: false), findsOneWidget); |
| |
| // Doesn't crash. |
| expect(tester.takeException(), isNull); |
| |
| rootNavigator.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Foo'), findsNothing); |
| // Both heroes are in the tree, one is offstage |
| expect(find.byKey(nestedRouteHeroTop), findsOneWidget); |
| expect(find.byKey(nestedRouteHeroBottom), findsNothing); |
| expect(find.byKey(nestedRouteHeroBottom, skipOffstage: false), findsOneWidget); |
| }); |
| |
| testWidgets('Can hero from route in root Navigator to route in nested Navigator', (WidgetTester tester) async { |
| const String heroTag = 'foo'; |
| final GlobalKey<NavigatorState> rootNavigator = GlobalKey(); |
| final Key smallContainer = UniqueKey(); |
| final Key largeContainer = UniqueKey(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: rootNavigator, |
| home: Center( |
| child: Card( |
| child: Hero( |
| tag: heroTag, |
| child: Container( |
| key: largeContainer, |
| color: Colors.red, |
| height: 200.0, |
| width: 200.0, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| |
| // The initial setup. |
| expect(find.byKey(largeContainer), isOnstage); |
| expect(find.byKey(largeContainer), isInCard); |
| expect(find.byKey(smallContainer, skipOffstage: false), findsNothing); |
| |
| rootNavigator.currentState!.push( |
| MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Center( |
| child: Card( |
| child: Hero( |
| tag: heroTag, |
| child: Container( |
| key: smallContainer, |
| color: Colors.red, |
| height: 100.0, |
| width: 100.0, |
| ), |
| ), |
| ), |
| ); |
| }, |
| ), |
| ); |
| await tester.pump(); |
| |
| // The second route exists offstage. |
| expect(find.byKey(largeContainer), isOnstage); |
| expect(find.byKey(largeContainer), isInCard); |
| expect(find.byKey(smallContainer, skipOffstage: false), isOffstage); |
| expect(find.byKey(smallContainer, skipOffstage: false), isInCard); |
| |
| await tester.pump(); |
| |
| // The hero started flying. |
| expect(find.byKey(largeContainer), findsNothing); |
| expect(find.byKey(smallContainer), isOnstage); |
| expect(find.byKey(smallContainer), isNotInCard); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| |
| // The hero is in-flight. |
| expect(find.byKey(largeContainer), findsNothing); |
| expect(find.byKey(smallContainer), isOnstage); |
| expect(find.byKey(smallContainer), isNotInCard); |
| final Size size = tester.getSize(find.byKey(smallContainer)); |
| expect(size.height, greaterThan(100)); |
| expect(size.width, greaterThan(100)); |
| expect(size.height, lessThan(200)); |
| expect(size.width, lessThan(200)); |
| |
| await tester.pumpAndSettle(); |
| |
| // The transition has ended. |
| expect(find.byKey(largeContainer), findsNothing); |
| expect(find.byKey(smallContainer), isOnstage); |
| expect(find.byKey(smallContainer), isInCard); |
| expect(tester.getSize(find.byKey(smallContainer)), const Size(100,100)); |
| }); |
| |
| testWidgets('Hero within a Hero, throws', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Hero( |
| tag: 'a', |
| child: Hero( |
| tag: 'b', |
| child: Text('Child of a Hero'), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.takeException(), isAssertionError); |
| }); |
| |
| testWidgets('Can push/pop on outer Navigator if nested Navigators contains same Heroes', (WidgetTester tester) async { |
| const String heroTag = 'foo'; |
| final GlobalKey<NavigatorState> rootNavigator = GlobalKey<NavigatorState>(); |
| final Key rootRouteHero = UniqueKey(); |
| final Key nestedRouteHeroOne = UniqueKey(); |
| final Key nestedRouteHeroTwo = UniqueKey(); |
| final List<Key> keys = <Key>[nestedRouteHeroOne, nestedRouteHeroTwo]; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| navigatorKey: rootNavigator, |
| home: CupertinoTabScaffold( |
| tabBar: CupertinoTabBar( |
| items: const <BottomNavigationBarItem>[ |
| BottomNavigationBarItem(icon: Icon(Icons.home)), |
| BottomNavigationBarItem(icon: Icon(Icons.favorite)), |
| ], |
| ), |
| tabBuilder: (BuildContext context, int index) { |
| return CupertinoTabView( |
| builder: (BuildContext context) => Hero( |
| tag: heroTag, |
| child: Placeholder( |
| key: keys[index], |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ); |
| |
| // Show both tabs to init. |
| await tester.tap(find.byIcon(Icons.home)); |
| await tester.pump(); |
| |
| await tester.tap(find.byIcon(Icons.favorite)); |
| await tester.pump(); |
| |
| // Inner heroes are in the tree, one is offstage. |
| expect(find.byKey(nestedRouteHeroTwo), findsOneWidget); |
| expect(find.byKey(nestedRouteHeroOne), findsNothing); |
| expect(find.byKey(nestedRouteHeroOne, skipOffstage: false), findsOneWidget); |
| |
| // Root hero is not in the tree. |
| expect(find.byKey(rootRouteHero), findsNothing); |
| |
| rootNavigator.currentState!.push( |
| MaterialPageRoute<void>( |
| builder: (BuildContext context) => Hero( |
| tag: heroTag, |
| child: Placeholder( |
| key: rootRouteHero, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pumpAndSettle(); |
| |
| // Inner heroes are still in the tree, both are offstage. |
| expect(find.byKey(nestedRouteHeroOne), findsNothing); |
| expect(find.byKey(nestedRouteHeroTwo), findsNothing); |
| expect(find.byKey(nestedRouteHeroOne, skipOffstage: false), findsOneWidget); |
| expect(find.byKey(nestedRouteHeroTwo, skipOffstage: false), findsOneWidget); |
| |
| // Root hero is in the tree. |
| expect(find.byKey(rootRouteHero), findsOneWidget); |
| |
| // Doesn't crash. |
| expect(tester.takeException(), isNull); |
| |
| rootNavigator.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| |
| // Root hero is not in the tree |
| expect(find.byKey(rootRouteHero), findsNothing); |
| |
| // Both heroes are in the tree, one is offstage |
| expect(find.byKey(nestedRouteHeroTwo), findsOneWidget); |
| expect(find.byKey(nestedRouteHeroOne), findsNothing); |
| expect(find.byKey(nestedRouteHeroOne, skipOffstage: false), findsOneWidget); |
| }); |
| |
| testWidgets('Hero within a Hero subtree, throws', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Hero( |
| tag: 'a', |
| child: Hero( |
| tag: 'b', |
| child: Text('Child of a Hero'), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.takeException(), isAssertionError); |
| }); |
| |
| testWidgets('Hero within a Hero subtree with Builder, throws', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Hero( |
| tag: 'a', |
| child: Builder( |
| builder: (BuildContext context) { |
| return const Hero( |
| tag: 'b', |
| child: Text('Child of a Hero'), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.takeException(),isAssertionError); |
| }); |
| |
| testWidgets('Hero within a Hero subtree with LayoutBuilder, throws', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Hero( |
| tag: 'a', |
| child: LayoutBuilder( |
| builder: (BuildContext context, BoxConstraints constraints) { |
| return const Hero( |
| tag: 'b', |
| child: Text('Child of a Hero'), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.takeException(), isAssertionError); |
| }); |
| |
| testWidgets('Heroes fly on pushReplacement', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/28041. |
| |
| const String heroTag = 'foo'; |
| final GlobalKey<NavigatorState> navigator = GlobalKey(); |
| final Key smallContainer = UniqueKey(); |
| final Key largeContainer = UniqueKey(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: navigator, |
| home: Center( |
| child: Card( |
| child: Hero( |
| tag: heroTag, |
| child: Container( |
| key: largeContainer, |
| color: Colors.red, |
| height: 200.0, |
| width: 200.0, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // The initial setup. |
| expect(find.byKey(largeContainer), isOnstage); |
| expect(find.byKey(largeContainer), isInCard); |
| expect(find.byKey(smallContainer, skipOffstage: false), findsNothing); |
| |
| navigator.currentState!.pushReplacement( |
| MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Center( |
| child: Card( |
| child: Hero( |
| tag: heroTag, |
| child: Container( |
| key: smallContainer, |
| color: Colors.red, |
| height: 100.0, |
| width: 100.0, |
| ), |
| ), |
| ), |
| ); |
| }, |
| ), |
| ); |
| await tester.pump(); |
| |
| // The second route exists offstage. |
| expect(find.byKey(largeContainer), isOnstage); |
| expect(find.byKey(largeContainer), isInCard); |
| expect(find.byKey(smallContainer, skipOffstage: false), isOffstage); |
| expect(find.byKey(smallContainer, skipOffstage: false), isInCard); |
| |
| await tester.pump(); |
| |
| // The hero started flying. |
| expect(find.byKey(largeContainer), findsNothing); |
| expect(find.byKey(smallContainer), isOnstage); |
| expect(find.byKey(smallContainer), isNotInCard); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| |
| // The hero is in-flight. |
| expect(find.byKey(largeContainer), findsNothing); |
| expect(find.byKey(smallContainer), isOnstage); |
| expect(find.byKey(smallContainer), isNotInCard); |
| final Size size = tester.getSize(find.byKey(smallContainer)); |
| expect(size.height, greaterThan(100)); |
| expect(size.width, greaterThan(100)); |
| expect(size.height, lessThan(200)); |
| expect(size.width, lessThan(200)); |
| |
| await tester.pumpAndSettle(); |
| |
| // The transition has ended. |
| expect(find.byKey(largeContainer), findsNothing); |
| expect(find.byKey(smallContainer), isOnstage); |
| expect(find.byKey(smallContainer), isInCard); |
| expect(tester.getSize(find.byKey(smallContainer)), const Size(100,100)); |
| }); |
| |
| testWidgets('On an iOS back swipe and snap, only a single flight should take place', (WidgetTester tester) async { |
| int shuttlesBuilt = 0; |
| Widget shuttleBuilder( |
| BuildContext flightContext, |
| Animation<double> animation, |
| HeroFlightDirection flightDirection, |
| BuildContext fromHeroContext, |
| BuildContext toHeroContext, |
| ) { |
| shuttlesBuilt += 1; |
| return const Text("I'm flying in a jetplane"); |
| } |
| |
| final GlobalKey<NavigatorState> navigatorKey = GlobalKey(); |
| await tester.pumpWidget( |
| CupertinoApp( |
| navigatorKey: navigatorKey, |
| home: Hero( |
| tag: navigatorKey, |
| // Since we're popping, only the destination route's builder is used. |
| flightShuttleBuilder: shuttleBuilder, |
| transitionOnUserGestures: true, |
| child: const Text('1'), |
| ), |
| ), |
| ); |
| |
| final CupertinoPageRoute<void> route2 = CupertinoPageRoute<void>( |
| builder: (BuildContext context) { |
| return CupertinoPageScaffold( |
| child: Hero( |
| tag: navigatorKey, |
| transitionOnUserGestures: true, |
| child: const Text('2'), |
| ), |
| ); |
| }, |
| ); |
| |
| navigatorKey.currentState!.push(route2); |
| await tester.pumpAndSettle(); |
| |
| expect(shuttlesBuilt, 1); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0)); |
| await gesture.moveBy(const Offset(500.0, 0.0)); |
| await tester.pump(); |
| // Starting the back swipe creates a new hero shuttle. |
| expect(shuttlesBuilt, 2); |
| |
| await gesture.up(); |
| await tester.pump(); |
| // After the lift, no additional shuttles should be created since it's the |
| // same hero flight. |
| expect(shuttlesBuilt, 2); |
| |
| // Did go far enough to snap out of this route. |
| await tester.pump(const Duration(milliseconds: 301)); |
| expect(find.text('2'), findsNothing); |
| // Still one shuttle. |
| expect(shuttlesBuilt, 2); |
| }); |
| |
| testWidgets( |
| "From hero's state should be preserved, " |
| 'heroes work well with child widgets that has global keys', |
| (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> navigatorKey = GlobalKey(); |
| final GlobalKey<_SimpleState> key1 = GlobalKey<_SimpleState>(); |
| final GlobalKey key2 = GlobalKey(); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| navigatorKey: navigatorKey, |
| home: Row( |
| crossAxisAlignment: CrossAxisAlignment.start, |
| children: <Widget>[ |
| Hero( |
| tag: 'hero', |
| transitionOnUserGestures: true, |
| child: _SimpleStatefulWidget(key: key1), |
| ), |
| const SizedBox( |
| width: 10, |
| height: 10, |
| child: Text('1'), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| final CupertinoPageRoute<void> route2 = CupertinoPageRoute<void>( |
| builder: (BuildContext context) { |
| return CupertinoPageScaffold( |
| child: Hero( |
| tag: 'hero', |
| transitionOnUserGestures: true, |
| // key2 is a `GlobalKey`. The hero animation should not |
| // assert by having the same global keyed widget in more |
| // than one place in the tree. |
| child: _SimpleStatefulWidget(key: key2), |
| ), |
| ); |
| }, |
| ); |
| |
| final _SimpleState state1 = key1.currentState!; |
| state1.state = 1; |
| |
| navigatorKey.currentState!.push(route2); |
| await tester.pump(); |
| |
| expect(state1.mounted, isTrue); |
| |
| await tester.pumpAndSettle(); |
| expect(state1.state, 1); |
| // The element should be mounted and unique. |
| expect(state1.mounted, isTrue); |
| |
| navigatorKey.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| |
| // State is preserved. |
| expect(state1.state, 1); |
| // The element should be mounted and unique. |
| expect(state1.mounted, isTrue); |
| }, |
| ); |
| |
| testWidgets( |
| "Hero works with images that don't have both width and height specified", |
| // Regression test for https://github.com/flutter/flutter/issues/32356 |
| // and https://github.com/flutter/flutter/issues/31503 |
| (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> navigatorKey = GlobalKey(); |
| const Key imageKey1 = Key('image1'); |
| const Key imageKey2 = Key('image2'); |
| final TestImageProvider imageProvider = TestImageProvider(testImage); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| navigatorKey: navigatorKey, |
| home: Row( |
| crossAxisAlignment: CrossAxisAlignment.start, |
| children: <Widget>[ |
| Hero( |
| tag: 'hero', |
| transitionOnUserGestures: true, |
| child: SizedBox( |
| width: 100, |
| child: Image( |
| image: imageProvider, |
| key: imageKey1, |
| ), |
| ), |
| ), |
| const SizedBox( |
| width: 10, |
| height: 10, |
| child: Text('1'), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| final CupertinoPageRoute<void> route2 = CupertinoPageRoute<void>( |
| builder: (BuildContext context) { |
| return CupertinoPageScaffold( |
| child: Hero( |
| tag: 'hero', |
| transitionOnUserGestures: true, |
| child: Image( |
| image: imageProvider, |
| key: imageKey2, |
| ), |
| ), |
| ); |
| }, |
| ); |
| |
| // Load image before measuring the `Rect` of the `RenderImage`. |
| imageProvider.complete(); |
| await tester.pump(); |
| final RenderImage renderImage = tester.renderObject( |
| find.descendant(of: find.byKey(imageKey1), matching: find.byType(RawImage)), |
| ); |
| |
| // Before push image1 should be laid out correctly. |
| expect(renderImage.size, const Size(100, 100)); |
| |
| navigatorKey.currentState!.push(route2); |
| await tester.pump(); |
| |
| final TestGesture gesture = await tester.startGesture(const Offset(0.01, 300)); |
| await tester.pump(); |
| |
| // Move (almost) across the screen, to make the animation as close to finish |
| // as possible. |
| await gesture.moveTo(const Offset(800, 200)); |
| await tester.pump(); |
| |
| // image1 should snap to the top left corner of the Row widget. |
| expect( |
| tester.getRect(find.byKey(imageKey1, skipOffstage: false)), |
| rectMoreOrLessEquals(tester.getTopLeft(find.widgetWithText(Row, '1')) & const Size(100, 100), epsilon: 0.01), |
| ); |
| |
| // Text should respect the correct final size of image1. |
| expect( |
| tester.getTopRight(find.byKey(imageKey1, skipOffstage: false)).dx, |
| moreOrLessEquals(tester.getTopLeft(find.text('1')).dx, epsilon: 0.01), |
| ); |
| }, |
| ); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/38183. |
| testWidgets('Remove user gesture driven flights when the gesture is invalid', (WidgetTester tester) async { |
| transitionFromUserGestures = true; |
| await tester.pumpWidget(MaterialApp( |
| routes: routes, |
| )); |
| |
| await tester.tap(find.text('simple')); |
| await tester.pump(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byKey(simpleKey), findsOneWidget); |
| |
| // Tap once to trigger a flight. |
| await tester.tapAt(const Offset(10, 200)); |
| await tester.pumpAndSettle(); |
| |
| // Wait till the previous gesture is accepted. |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| // Tap again to trigger another flight, see if it throws. |
| await tester.tapAt(const Offset(10, 200)); |
| await tester.pumpAndSettle(); |
| |
| // The simple route should still be on top. |
| expect(find.byKey(simpleKey), findsOneWidget); |
| expect(tester.takeException(), isNull); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/40239. |
| testWidgets( |
| 'In a pop transition, when fromHero is null, the to hero should eventually become visible', |
| (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> navigatorKey = GlobalKey(); |
| late StateSetter setState; |
| bool shouldDisplayHero = true; |
| await tester.pumpWidget( |
| CupertinoApp( |
| navigatorKey: navigatorKey, |
| home: Hero( |
| tag: navigatorKey, |
| child: const Placeholder(), |
| ), |
| ), |
| ); |
| |
| final CupertinoPageRoute<void> route2 = CupertinoPageRoute<void>( |
| builder: (BuildContext context) { |
| return CupertinoPageScaffold( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setter) { |
| setState = setter; |
| return shouldDisplayHero |
| ? Hero(tag: navigatorKey, child: const Text('text')) |
| : const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ); |
| |
| navigatorKey.currentState!.push(route2); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('text'), findsOneWidget); |
| expect(find.byType(Placeholder), findsNothing); |
| |
| setState(() { shouldDisplayHero = false; }); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('text'), findsNothing); |
| |
| navigatorKey.currentState!.pop(); |
| await tester.pumpAndSettle(); |
| |
| expect(find.byType(Placeholder), findsOneWidget); |
| }, |
| ); |
| |
| testWidgets('popped hero uses fastOutSlowIn curve', (WidgetTester tester) async { |
| final Key container1 = UniqueKey(); |
| final Key container2 = UniqueKey(); |
| final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); |
| |
| final Animatable<Size?> tween = SizeTween( |
| begin: const Size(200, 200), |
| end: const Size(100, 100), |
| ).chain(CurveTween(curve: Curves.fastOutSlowIn)); |
| |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: navigator, |
| home: Scaffold( |
| body: Center( |
| child: Hero( |
| tag: 'test', |
| createRectTween: (Rect? begin, Rect? end) { |
| return RectTween(begin: begin, end: end); |
| }, |
| child: SizedBox( |
| key: container1, |
| height: 100, |
| width: 100, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| final Size originalSize = tester.getSize(find.byKey(container1)); |
| expect(originalSize, const Size(100, 100)); |
| |
| navigator.currentState!.push(MaterialPageRoute<void>(builder: (BuildContext context) { |
| return Scaffold( |
| body: Center( |
| child: Hero( |
| tag: 'test', |
| createRectTween: (Rect? begin, Rect? end) { |
| return RectTween(begin: begin, end: end); |
| }, |
| child: SizedBox( |
| key: container2, |
| height: 200, |
| width: 200, |
| ), |
| ), |
| ), |
| ); |
| })); |
| await tester.pumpAndSettle(); |
| final Size newSize = tester.getSize(find.byKey(container2)); |
| expect(newSize, const Size(200, 200)); |
| |
| navigator.currentState!.pop(); |
| await tester.pump(); |
| |
| // Jump 25% into the transition (total length = 300ms) |
| await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms |
| Size heroSize = tester.getSize(find.byKey(container1)); |
| expect(heroSize, tween.transform(0.25)); |
| |
| // Jump to 50% into the transition. |
| await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms |
| heroSize = tester.getSize(find.byKey(container1)); |
| expect(heroSize, tween.transform(0.50)); |
| |
| // Jump to 75% into the transition. |
| await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms |
| heroSize = tester.getSize(find.byKey(container1)); |
| expect(heroSize, tween.transform(0.75)); |
| |
| // Jump to 100% into the transition. |
| await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms |
| heroSize = tester.getSize(find.byKey(container1)); |
| expect(heroSize, tween.transform(1.0)); |
| }); |
| |
| testWidgets('Heroes in enabled HeroMode do transition', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: Column( |
| children: <Widget>[ |
| HeroMode( |
| enabled: true, |
| child: Card( |
| child: Hero( |
| tag: 'a', |
| child: SizedBox( |
| height: 100.0, |
| width: 100.0, |
| key: firstKey, |
| ), |
| ), |
| ), |
| ), |
| Builder( |
| builder: (BuildContext context) { |
| return TextButton( |
| child: const Text('push'), |
| onPressed: () { |
| Navigator.push(context, PageRouteBuilder<void>( |
| pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { |
| return Card( |
| child: Hero( |
| tag: 'a', |
| child: SizedBox( |
| height: 150.0, |
| width: 150.0, |
| key: secondKey, |
| ), |
| ), |
| ); |
| }, |
| )); |
| }, |
| ); |
| }, |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| |
| await tester.tap(find.text('push')); |
| await tester.pump(); |
| |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey, skipOffstage: false), isOffstage); |
| expect(find.byKey(secondKey, skipOffstage: false), isInCard); |
| |
| await tester.pump(); |
| |
| expect(find.byKey(firstKey), findsNothing); |
| expect(find.byKey(secondKey), findsOneWidget); |
| expect(find.byKey(secondKey), isNotInCard); |
| expect(find.byKey(secondKey), isOnstage); |
| |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(find.byKey(firstKey), findsNothing); |
| expect(find.byKey(secondKey), isOnstage); |
| expect(find.byKey(secondKey), isInCard); |
| }); |
| |
| testWidgets('Heroes in disabled HeroMode do not transition', (WidgetTester tester) async { |
| await tester.pumpWidget(MaterialApp( |
| home: Material( |
| child: Column( |
| children: <Widget>[ |
| HeroMode( |
| enabled: false, |
| child: Card( |
| child: Hero( |
| tag: 'a', |
| child: SizedBox( |
| height: 100.0, |
| width: 100.0, |
| key: firstKey, |
| ), |
| ), |
| ), |
| ), |
| Builder( |
| builder: (BuildContext context) { |
| return TextButton( |
| child: const Text('push'), |
| onPressed: () { |
| Navigator.push(context, PageRouteBuilder<void>( |
| pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { |
| return Card( |
| child: Hero( |
| tag: 'a', |
| child: SizedBox( |
| height: 150.0, |
| width: 150.0, |
| key: secondKey, |
| ), |
| ), |
| ); |
| }, |
| )); |
| }, |
| ); |
| }, |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey), findsNothing); |
| |
| await tester.tap(find.text('push')); |
| await tester.pump(); |
| |
| expect(find.byKey(firstKey), isOnstage); |
| expect(find.byKey(firstKey), isInCard); |
| expect(find.byKey(secondKey, skipOffstage: false), isOffstage); |
| expect(find.byKey(secondKey, skipOffstage: false), isInCard); |
| |
| await tester.pump(); |
| |
| // When HeroMode is disabled, heroes will not move. |
| // So the original page contains the hero. |
| expect(find.byKey(firstKey), findsOneWidget); |
| |
| // The hero should be in the new page, onstage, soon. |
| expect(find.byKey(secondKey), findsOneWidget); |
| expect(find.byKey(secondKey), isInCard); |
| expect(find.byKey(secondKey), isOnstage); |
| |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(find.byKey(firstKey), findsNothing); |
| |
| expect(find.byKey(secondKey), findsOneWidget); |
| expect(find.byKey(secondKey), isInCard); |
| expect(find.byKey(secondKey), isOnstage); |
| }); |
| |
| testWidgets('kept alive Hero does not throw when the transition begins', (WidgetTester tester) async { |
| final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| navigatorKey: navigatorKey, |
| home: Scaffold( |
| body: ListView( |
| addAutomaticKeepAlives: false, |
| addRepaintBoundaries: false, |
| addSemanticIndexes: false, |
| children: <Widget>[ |
| const KeepAlive( |
| keepAlive: true, |
| child: Hero( |
| tag: 'a', |
| child: Placeholder(), |
| ), |
| ), |
| Container(height: 1000.0), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| // Scroll to make the Hero invisible. |
| await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); |
| await tester.pump(); |
| |
| expect(find.byType(TextField), findsNothing); |
| |
| navigatorKey.currentState?.push( |
| MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return const Scaffold( |
| body: Center( |
| |