diff --git a/examples/api/lib/widgets/heroes/hero.0.dart b/examples/api/lib/widgets/heroes/hero.0.dart index 0014789d4f6..2adce8d6762 100644 --- a/examples/api/lib/widgets/heroes/hero.0.dart +++ b/examples/api/lib/widgets/heroes/hero.0.dart @@ -6,29 +6,26 @@ import 'package:flutter/material.dart'; -void main() => runApp(const MyApp()); +void main() => runApp(const HeroApp()); -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); - - static const String _title = 'Flutter Code Sample'; +class HeroApp extends StatelessWidget { + const HeroApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( - title: _title, home: Scaffold( - appBar: AppBar(title: const Text(_title)), + appBar: AppBar(title: const Text('Hero Sample')), body: const Center( - child: MyStatelessWidget(), + child: HeroExample(), ), ), ); } } -class MyStatelessWidget extends StatelessWidget { - const MyStatelessWidget({Key? key}) : super(key: key); +class HeroExample extends StatelessWidget { + const HeroExample({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -41,17 +38,18 @@ class MyStatelessWidget extends StatelessWidget { ListTile( leading: Hero( tag: 'hero-rectangle', - child: _blueRectangle(const Size(50, 50)), + child: _box(const Size(50, 50)), ), onTap: () => _gotoDetailsPage(context), - title: - const Text('Tap on the icon to view hero animation transition.'), + title: const Text( + 'Tap on the icon to view hero animation transition.', + ), ), ], ); } - Widget _blueRectangle(Size size) { + Widget _box(Size size) { return Container( width: size.width, height: size.height, @@ -63,7 +61,7 @@ class MyStatelessWidget extends StatelessWidget { Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) => Scaffold( appBar: AppBar( - title: const Text('second Page'), + title: const Text('Second Page'), ), body: Center( child: Column( @@ -71,7 +69,7 @@ class MyStatelessWidget extends StatelessWidget { children: [ Hero( tag: 'hero-rectangle', - child: _blueRectangle(const Size(200, 200)), + child: _box(const Size(200, 200)), ), ], ), diff --git a/examples/api/lib/widgets/heroes/hero.1.dart b/examples/api/lib/widgets/heroes/hero.1.dart new file mode 100644 index 00000000000..94255a88636 --- /dev/null +++ b/examples/api/lib/widgets/heroes/hero.1.dart @@ -0,0 +1,110 @@ +// 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. + +// Flutter code sample for Hero + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +void main() { + // Slow down time to see Hero flight animation. + timeDilation = 15.0; + runApp(const HeroApp()); +} + +class HeroApp extends StatelessWidget { + const HeroApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Hero Sample')), + body: const Center( + child: HeroExample(), + ), + ), + ); + } +} + +class HeroExample extends StatelessWidget { + const HeroExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ListTile( + leading: Hero( + tag: 'hero-default-tween', + child: _box(size: 50.0, color: Colors.red[700]!.withOpacity(0.5)), + ), + title: const Text( + 'This red icon will use a default rect tween during the hero flight.', + ), + ), + const SizedBox(height: 10.0), + ListTile( + leading: Hero( + tag: 'hero-custom-tween', + createRectTween: (Rect? begin, Rect? end) { + return MaterialRectCenterArcTween(begin: begin, end: end); + }, + child: _box(size: 50.0, color: Colors.blue[700]!.withOpacity(0.5)), + ), + title: const Text( + 'This blue icon will use a custom rect tween during the hero flight.', + ), + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () => _gotoDetailsPage(context), + child: const Text('Tap to trigger hero flight'), + ), + ], + ); + } + + Widget _box({double? size, Color? color}) { + return Container( + color: color, + child: FlutterLogo(size: size), + ); + } + + void _gotoDetailsPage(BuildContext context) { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Second Page'), + ), + body: Align( + alignment: Alignment.bottomRight, + child: Stack( + children: [ + Hero( + tag: 'hero-custom-tween', + createRectTween: (Rect? begin, Rect? end) { + return MaterialRectCenterArcTween(begin: begin, end: end); + }, + child: _box( + size: 400.0, + color: Colors.blue[700]!.withOpacity(0.5), + ), + ), + Hero( + tag: 'hero-default-tween', + child: _box( + size: 400.0, + color: Colors.red[700]!.withOpacity(0.5), + ), + ), + ], + ), + ), + ), + )); + } +} diff --git a/examples/api/test/widgets/heroes/hero.0_test.dart b/examples/api/test/widgets/heroes/hero.0_test.dart new file mode 100644 index 00000000000..058436ddb09 --- /dev/null +++ b/examples/api/test/widgets/heroes/hero.0_test.dart @@ -0,0 +1,71 @@ +// 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 'package:flutter/material.dart'; +import 'package:flutter_api_samples/widgets/heroes/hero.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Has Hero animation', (WidgetTester tester) async { + await tester.pumpWidget( + const example.HeroApp(), + ); + + expect(find.text('Hero Sample'), findsOneWidget); + await tester.tap(find.byType(Container)); + await tester.pump(); + + Size heroSize = tester.getSize(find.byType(Container)); + + // Jump 25% into the transition (total length = 300ms) + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container)); + expect(heroSize.width.roundToDouble(), 103.0); + expect(heroSize.height.roundToDouble(), 60.0); + + // Jump to 50% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container)); + expect(heroSize.width.roundToDouble(), 189.0); + expect(heroSize.height.roundToDouble(), 146.0); + + // Jump to 75% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container)); + expect(heroSize.width.roundToDouble(), 199.0); + expect(heroSize.height.roundToDouble(), 190.0); + + // Jump to 100% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container)); + expect(heroSize, const Size(200.0, 200.0)); + + expect(find.byIcon(Icons.arrow_back), findsOneWidget); + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pump(); + + // Jump 25% into the transition (total length = 300ms) + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container)); + expect(heroSize.width.roundToDouble(), 199.0); + expect(heroSize.height.roundToDouble(), 190.0); + + // Jump to 50% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container)); + expect(heroSize.width.roundToDouble(), 189.0); + expect(heroSize.height.roundToDouble(), 146.0); + + // Jump to 75% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container)); + expect(heroSize.width.roundToDouble(), 103.0); + expect(heroSize.height.roundToDouble(), 60.0); + + // Jump to 100% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container)); + expect(heroSize, const Size(50.0, 50.0)); + }); +} diff --git a/examples/api/test/widgets/heroes/hero.1_test.dart b/examples/api/test/widgets/heroes/hero.1_test.dart new file mode 100644 index 00000000000..18cd6011e70 --- /dev/null +++ b/examples/api/test/widgets/heroes/hero.1_test.dart @@ -0,0 +1,135 @@ +// 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 'package:flutter/material.dart'; +import 'package:flutter_api_samples/widgets/heroes/hero.1.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Hero flight animation with default rect tween', (WidgetTester tester) async { + await tester.pumpWidget( + const example.HeroApp(), + ); + + expect(find.text('Hero Sample'), findsOneWidget); + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + + Size heroSize = tester.getSize(find.byType(Container).first); + expect(heroSize, const Size(50.0, 50.0)); + + // Jump 25% into the transition (total length = 300ms) + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).first); + expect(heroSize.width.roundToDouble(), 171.0); + expect(heroSize.height.roundToDouble(), 73.0); + + // Jump to 50% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).first); + expect(heroSize.width.roundToDouble(), 371.0); + expect(heroSize.height.roundToDouble(), 273.0); + + // Jump to 75% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).first); + expect(heroSize.width.roundToDouble(), 398.0); + expect(heroSize.height.roundToDouble(), 376.0); + + // Jump to 100% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).first); + expect(heroSize, const Size(400.0, 400.0)); + + expect(find.byIcon(Icons.arrow_back), findsOneWidget); + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pump(); + + // Jump 25% into the transition (total length = 300ms) + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).first); + expect(heroSize.width.roundToDouble(), 398.0); + expect(heroSize.height.roundToDouble(), 376.0); + + // Jump to 50% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).first); + expect(heroSize.width.roundToDouble(), 371.0); + expect(heroSize.height.roundToDouble(), 273.0); + + // Jump to 75% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).first); + expect(heroSize.width.roundToDouble(), 171.0); + expect(heroSize.height.roundToDouble(), 73.0); + + // Jump to 100% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).first); + expect(heroSize, const Size(50.0, 50.0)); + }); + + testWidgets('Hero flight animation with custom rect tween', (WidgetTester tester) async { + await tester.pumpWidget( + const example.HeroApp(), + ); + + expect(find.text('Hero Sample'), findsOneWidget); + await tester.tap(find.byType(ElevatedButton)); + await tester.pump(); + + Size heroSize = tester.getSize(find.byType(Container).last); + expect(heroSize, const Size(50.0, 50.0)); + + // Jump 25% into the transition (total length = 300ms) + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).last); + expect(heroSize.width.roundToDouble(), 133.0); + expect(heroSize.height.roundToDouble(), 133.0); + + // Jump to 50% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).last); + expect(heroSize.width.roundToDouble(), 321.0); + expect(heroSize.height.roundToDouble(), 321.0); + + // Jump to 75% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).first); + expect(heroSize.width.roundToDouble(), 398.0); + expect(heroSize.height.roundToDouble(), 376.0); + + // Jump to 100% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).last); + expect(heroSize, const Size(400.0, 400.0)); + + expect(find.byIcon(Icons.arrow_back), findsOneWidget); + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pump(); + + // Jump 25% into the transition (total length = 300ms) + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).last); + expect(heroSize.width.roundToDouble(), 386.0); + expect(heroSize.height.roundToDouble(), 386.0); + + // Jump to 50% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).last); + expect(heroSize.width.roundToDouble(), 321.0); + expect(heroSize.height.roundToDouble(), 321.0); + + // Jump to 75% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).last); + expect(heroSize.width.roundToDouble(), 133.0); + expect(heroSize.height.roundToDouble(), 133.0); + + // Jump to 100% into the transition. + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + heroSize = tester.getSize(find.byType(Container).last); + expect(heroSize, const Size(50.0, 50.0)); + }); +} diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index 253a487ba1a..f44e06c8eb8 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -71,7 +71,6 @@ enum HeroFlightDirection { pop, } - /// A widget that marks its child as being a candidate for /// [hero animations](https://flutter.dev/docs/development/ui/animations/hero-animations). /// @@ -116,6 +115,13 @@ enum HeroFlightDirection { /// ** See code in examples/api/lib/widgets/heroes/hero.0.dart ** /// {@end-tool} /// +/// {@tool dartpad} +/// This sample shows [Hero] flight animations using default tween +/// and custom rect tween. +/// +/// ** See code in examples/api/lib/widgets/heroes/hero.1.dart ** +/// {@end-tool} +/// /// ## Discussion /// /// Heroes and the [Navigator]'s [Overlay] [Stack] must be axis-aligned for diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index 0a645884271..c4cbbaeb9ff 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -2649,7 +2649,6 @@ Future main() async { end: const Size(100, 100), ).chain(CurveTween(curve: Curves.fastOutSlowIn)); - await tester.pumpWidget( MaterialApp( navigatorKey: navigator,