From 11d2729db023a9467660b5a7887b59b678b2ec18 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Mon, 6 Jan 2020 12:05:26 -0800 Subject: [PATCH 01/29] WIP - Fade transition for dialogs --- packages/animations/lib/animations.dart | 1 + .../animations/lib/src/fade_transition.dart | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 packages/animations/lib/src/fade_transition.dart diff --git a/packages/animations/lib/animations.dart b/packages/animations/lib/animations.dart index ce0beba545c5..a87cbde9e2ea 100644 --- a/packages/animations/lib/animations.dart +++ b/packages/animations/lib/animations.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. export 'src/fade_through_transition.dart'; +export 'src/fade_transition.dart'; export 'src/open_container.dart'; export 'src/page_transition_switcher.dart'; export 'src/shared_z_axis_transition.dart'; diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart new file mode 100644 index 000000000000..fc890115cfc1 --- /dev/null +++ b/packages/animations/lib/src/fade_transition.dart @@ -0,0 +1,87 @@ +// Copyright 2019 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:animations/src/utils/curves.dart'; +import 'package:flutter/material.dart'; + +/// TODO: add documentation +Future showDialogWithFadeTransition({ + @required BuildContext context, + @required Widget child, + Color barrierColor = Colors.black54, + String barrierLabel, + bool barrierDismissible = true, +}) { + final ThemeData theme = Theme.of(context); + + // TODO: somehow control the scrim exit duration as well + return showGeneralDialog( + context: context, + barrierColor: Colors.black54, + barrierDismissible: barrierDismissible, + barrierLabel: barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel, + transitionDuration: const Duration(milliseconds: 150), + transitionBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget child) { + switch (animation.status) { + case AnimationStatus.forward: + return _EnterTransition( + animation: animation, + child: child, + ); + case AnimationStatus.dismissed: + case AnimationStatus.reverse: + case AnimationStatus.completed: + return FadeTransition( + opacity: animation, // should be over 75ms + child: child, + ); + } + return null; // unreachable + }, + child: child, + ); + }, + pageBuilder: (BuildContext buildContext, Animation animation, Animation secondaryAnimation) { + return SafeArea( + child: Builder( + builder: (BuildContext context) { + return theme != null + ? Theme(data: theme, child: child) + : child; + } + ), + ); + }, + ); +} + +class _EnterTransition extends StatelessWidget { + const _EnterTransition({ + this.animation, + this.child, + }); + + final Animation animation; + final Widget child; + + static Animatable fadeInTransition = CurveTween(curve: const Interval(0.0, 0.3)); + static Animatable scaleInTransition = Tween( + begin: 0.80, + end: 1.00, + ).chain(CurveTween(curve: decelerateEasing)); + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: fadeInTransition.animate(animation), + child: ScaleTransition( + scale: scaleInTransition.animate(animation), + child: child, + ), + ); + } +} From 7b276fed0ecc845728c6dfd90e8ef7b67a31d87f Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Mon, 6 Jan 2020 14:31:02 -0800 Subject: [PATCH 02/29] WIP - Refactor to extend PopupRoute --- .../animations/lib/src/fade_transition.dart | 115 ++++++++++++------ 1 file changed, 77 insertions(+), 38 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index fc890115cfc1..1892be753c94 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -8,45 +8,55 @@ import 'package:flutter/material.dart'; /// TODO: add documentation Future showDialogWithFadeTransition({ @required BuildContext context, - @required Widget child, - Color barrierColor = Colors.black54, - String barrierLabel, bool barrierDismissible = true, + String barrierLabel, + bool useRootNavigator = true, + Widget child, }) { - final ThemeData theme = Theme.of(context); - + barrierLabel = barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel; // TODO: somehow control the scrim exit duration as well - return showGeneralDialog( - context: context, - barrierColor: Colors.black54, + assert(useRootNavigator != null); + assert(!barrierDismissible || barrierLabel != null); + return Navigator.of(context, rootNavigator: useRootNavigator).push(FadeDialogRoute( barrierDismissible: barrierDismissible, - barrierLabel: barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel, - transitionDuration: const Duration(milliseconds: 150), - transitionBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { - return AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget child) { - switch (animation.status) { - case AnimationStatus.forward: - return _EnterTransition( - animation: animation, - child: child, - ); - case AnimationStatus.dismissed: - case AnimationStatus.reverse: - case AnimationStatus.completed: - return FadeTransition( - opacity: animation, // should be over 75ms - child: child, - ); - } - return null; // unreachable - }, - child: child, - ); - }, - pageBuilder: (BuildContext buildContext, Animation animation, Animation secondaryAnimation) { - return SafeArea( + barrierLabel: barrierLabel, + child: child, + )); +} + +class FadeDialogRoute extends PopupRoute { + FadeDialogRoute({ + bool barrierDismissible = true, + String barrierLabel, + RouteSettings settings, + this.child, + }) : assert(barrierDismissible != null), + _barrierDismissible = barrierDismissible, + _barrierLabel = barrierLabel, + super(settings: settings); + + + @override + bool get barrierDismissible => _barrierDismissible; + final bool _barrierDismissible; + + @override + String get barrierLabel => _barrierLabel; + final String _barrierLabel; + + @override + Color get barrierColor => Colors.black54; + + @override + Duration get transitionDuration => const Duration(milliseconds: 150); + + final Widget child; + + @override + Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { + final ThemeData theme = Theme.of(context); + return Semantics( + child: SafeArea( child: Builder( builder: (BuildContext context) { return theme != null @@ -54,9 +64,38 @@ Future showDialogWithFadeTransition({ : child; } ), - ); - }, - ); + ), + scopesRoute: true, + explicitChildNodes: true, + ); + } + + @override + Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget child) { + switch (animation.status) { + case AnimationStatus.forward: + return _EnterTransition( + animation: animation, + child: child, + ); + case AnimationStatus.dismissed: + case AnimationStatus.reverse: + case AnimationStatus.completed: + return FadeTransition( + opacity: CurveTween( + curve: const Interval(0.5, 1.0), + ).animate(animation), // should be over 75ms + child: child, + ); + } + return null; // unreachable + }, + child: child, + ); + } } class _EnterTransition extends StatelessWidget { From b652352e9760cb029c019978f2ce956d443a155e Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Fri, 10 Jan 2020 09:01:24 -0800 Subject: [PATCH 03/29] Implement fade_transition.dart --- .../animations/lib/src/fade_transition.dart | 112 ++++++++++++++++-- 1 file changed, 102 insertions(+), 10 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index 1892be753c94..ddb0dbe9ec42 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -5,8 +5,85 @@ import 'package:animations/src/utils/curves.dart'; import 'package:flutter/material.dart'; -/// TODO: add documentation -Future showDialogWithFadeTransition({ +/// Displays a modal above the current contents of the app. +/// +/// This function displays the [FadeModalRoute], which transitions in +/// with the Material fade transition. +/// +/// Content below the dialog is dimmed with a [ModalBarrier]. +/// +/// ```dart +/// /// Sample widget that uses [showModalWithFadeTransition]. +/// class MyHomePage extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: Center( +/// child: RaisedButton( +/// onPressed: () { +/// showModalWithFadeTransition( +/// context: context, +/// child: FlutterLogoDialog(), +/// ); +/// }, +/// child: Icon(Icons.add), +/// ), +/// ), +/// ); +/// } +/// } +/// +/// /// Displays a modal with the FlutterLogo on it. +/// class FlutterLogoDialog extends StatelessWidget { +/// FlutterLogoDialog(); +/// +/// @override +/// Widget build(BuildContext context) { +/// return Center( +/// child: ConstrainedBox( +/// constraints: BoxConstraints( +/// maxHeight: 500, +/// maxWidth: 500, +/// minHeight: 250, +/// minWidth: 250, +/// ), +/// child: Material( +/// child: Center(child: FlutterLogo(size: 250)), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// The `context` argument is used to look up the [Navigator] for the +/// dialog. It is only used when the method is called. Its corresponding widget +/// can be safely removed from the tree before the dialog is closed. +/// +/// The `useRootNavigator` argument is used to determine whether to push the +/// dialog to the [Navigator] furthest from or nearest to the given `context`. +/// By default, `useRootNavigator` is `true` and the dialog route created by +/// this method is pushed to the root navigator. +/// +/// If the application has multiple [Navigator] objects, it may be necessary to +/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the +/// dialog rather than just `Navigator.pop(context, result)`. +/// +/// The `barrierDismissible` argument is used to determine whether this route +/// can be dismissed by tapping the modal barrier. This argument defaults +/// to true. If `barrierDismissible` is true, a non-null `barrierLabel` must be +/// provided. +/// +/// The `barrierLabel` argument is the semantic label used for a dismissible +/// barrier. This argument defaults to "Dismiss". +/// +/// Returns a [Future] that resolves to the value (if any) that was passed to +/// [Navigator.pop] when the dialog was closed. +/// +/// See also: +/// +/// * [FadeModalRoute], which is the route that is built by this function. +Future showModalWithFadeTransition({ @required BuildContext context, bool barrierDismissible = true, String barrierLabel, @@ -14,22 +91,35 @@ Future showDialogWithFadeTransition({ Widget child, }) { barrierLabel = barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel; - // TODO: somehow control the scrim exit duration as well assert(useRootNavigator != null); assert(!barrierDismissible || barrierLabel != null); - return Navigator.of(context, rootNavigator: useRootNavigator).push(FadeDialogRoute( + return Navigator.of(context, rootNavigator: useRootNavigator).push(FadeModalRoute( barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, child: child, )); } -class FadeDialogRoute extends PopupRoute { - FadeDialogRoute({ +/// A modal route that overlays a widget on the current route with the Material +/// fade transition. +/// +/// The fade pattern is used for UI elements that enter or exit from within +/// the screen bounds. Elements that enter use a quick fade in and scale from +/// 80% to 100%. Elements that exit simply fade out. The scale animation is +/// only applied to entering elements to emphasize new content over old. +/// +/// See also: +/// +/// * [showDialogWithFadeTransition], which displays the dialog popup. +class FadeModalRoute extends PopupRoute { + /// Creates a [FadeModalRoute] route with the Material fade transition. + /// + /// [barrierDismissible] is true by default. + FadeModalRoute({ bool barrierDismissible = true, String barrierLabel, RouteSettings settings, - this.child, + @required this.child, }) : assert(barrierDismissible != null), _barrierDismissible = barrierDismissible, _barrierLabel = barrierLabel, @@ -50,6 +140,10 @@ class FadeDialogRoute extends PopupRoute { @override Duration get transitionDuration => const Duration(milliseconds: 150); + @override + Duration get reverseTransitionDuration => const Duration(milliseconds: 75); + + /// Builds the primary contents of the route. final Widget child; @override @@ -85,9 +179,7 @@ class FadeDialogRoute extends PopupRoute { case AnimationStatus.reverse: case AnimationStatus.completed: return FadeTransition( - opacity: CurveTween( - curve: const Interval(0.5, 1.0), - ).animate(animation), // should be over 75ms + opacity: animation, child: child, ); } From f043bffccba1d9764e59ae5a08ec497a53c83fcc Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Fri, 10 Jan 2020 09:13:22 -0800 Subject: [PATCH 04/29] Simple test --- .../animations/test/fade_transition_test.dart | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 packages/animations/test/fade_transition_test.dart diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart new file mode 100644 index 000000000000..1332f2d194d5 --- /dev/null +++ b/packages/animations/test/fade_transition_test.dart @@ -0,0 +1,61 @@ +// Copyright 2019 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:animations/src/fade_transition.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + testWidgets( + 'showModalWithFadeTransition builds a new route', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return Center( + child: RaisedButton( + onPressed: () { + showModalWithFadeTransition( + context: context, + child: const _FlutterLogoDialog(), + ); + }, + child: Icon(Icons.add), + ), + ); + } + ), + ), + ), + ); + await tester.tap(find.byType(RaisedButton)); + await tester.pumpAndSettle(); + expect(find.byType(_FlutterLogoDialog), findsOneWidget); + }, + ); +} + +class _FlutterLogoDialog extends StatelessWidget { + const _FlutterLogoDialog(); + + @override + Widget build(BuildContext context) { + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 500, + maxWidth: 500, + minHeight: 250, + minWidth: 250, + ), + child: const Material( + child: Center(child: FlutterLogo(size: 250)), + ), + ), + ); + } +} \ No newline at end of file From 4f324415eb0bfd45743d2053ed40057fad3a356b Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Fri, 10 Jan 2020 09:17:11 -0800 Subject: [PATCH 05/29] Newline --- packages/animations/test/fade_transition_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index 1332f2d194d5..ab6e83c23f5c 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -58,4 +58,4 @@ class _FlutterLogoDialog extends StatelessWidget { ), ); } -} \ No newline at end of file +} From 2c726e0ed9a824122eec3c4f1e425c0151e59e11 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Fri, 10 Jan 2020 09:24:00 -0800 Subject: [PATCH 06/29] Remove unnecessary `settings` param, API wordsmithing --- packages/animations/lib/src/fade_transition.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index ddb0dbe9ec42..4dbe1589c8b6 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -118,13 +118,10 @@ class FadeModalRoute extends PopupRoute { FadeModalRoute({ bool barrierDismissible = true, String barrierLabel, - RouteSettings settings, @required this.child, }) : assert(barrierDismissible != null), _barrierDismissible = barrierDismissible, - _barrierLabel = barrierLabel, - super(settings: settings); - + _barrierLabel = barrierLabel; @override bool get barrierDismissible => _barrierDismissible; @@ -143,7 +140,7 @@ class FadeModalRoute extends PopupRoute { @override Duration get reverseTransitionDuration => const Duration(milliseconds: 75); - /// Builds the primary contents of the route. + /// The primary contents of the modal. final Widget child; @override From 2acc1ebfabfef9ccc4afb43d88f6283eba1bfaf6 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Fri, 10 Jan 2020 09:26:46 -0800 Subject: [PATCH 07/29] dialog -> modal --- .../animations/lib/src/fade_transition.dart | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index 4dbe1589c8b6..d8639e1b48f0 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; /// This function displays the [FadeModalRoute], which transitions in /// with the Material fade transition. /// -/// Content below the dialog is dimmed with a [ModalBarrier]. +/// Content below the modal is dimmed with a [ModalBarrier]. /// /// ```dart /// /// Sample widget that uses [showModalWithFadeTransition]. @@ -23,7 +23,7 @@ import 'package:flutter/material.dart'; /// onPressed: () { /// showModalWithFadeTransition( /// context: context, -/// child: FlutterLogoDialog(), +/// child: FlutterLogoModal(), /// ); /// }, /// child: Icon(Icons.add), @@ -34,20 +34,20 @@ import 'package:flutter/material.dart'; /// } /// /// /// Displays a modal with the FlutterLogo on it. -/// class FlutterLogoDialog extends StatelessWidget { -/// FlutterLogoDialog(); +/// class FlutterLogoModal extends StatelessWidget { +/// FlutterLogoModal(); /// /// @override /// Widget build(BuildContext context) { /// return Center( /// child: ConstrainedBox( -/// constraints: BoxConstraints( +/// constraints: const BoxConstraints( /// maxHeight: 500, /// maxWidth: 500, /// minHeight: 250, /// minWidth: 250, /// ), -/// child: Material( +/// child: const Material( /// child: Center(child: FlutterLogo(size: 250)), /// ), /// ), @@ -57,17 +57,17 @@ import 'package:flutter/material.dart'; /// ``` /// /// The `context` argument is used to look up the [Navigator] for the -/// dialog. It is only used when the method is called. Its corresponding widget -/// can be safely removed from the tree before the dialog is closed. +/// modal. It is only used when the method is called. Its corresponding widget +/// can be safely removed from the tree before the modal is closed. /// /// The `useRootNavigator` argument is used to determine whether to push the -/// dialog to the [Navigator] furthest from or nearest to the given `context`. -/// By default, `useRootNavigator` is `true` and the dialog route created by +/// modal to the [Navigator] furthest from or nearest to the given `context`. +/// By default, `useRootNavigator` is `true` and the modal route created by /// this method is pushed to the root navigator. /// /// If the application has multiple [Navigator] objects, it may be necessary to /// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the -/// dialog rather than just `Navigator.pop(context, result)`. +/// modal rather than just `Navigator.pop(context, result)`. /// /// The `barrierDismissible` argument is used to determine whether this route /// can be dismissed by tapping the modal barrier. This argument defaults @@ -78,7 +78,7 @@ import 'package:flutter/material.dart'; /// barrier. This argument defaults to "Dismiss". /// /// Returns a [Future] that resolves to the value (if any) that was passed to -/// [Navigator.pop] when the dialog was closed. +/// [Navigator.pop] when the modal was closed. /// /// See also: /// @@ -110,7 +110,7 @@ Future showModalWithFadeTransition({ /// /// See also: /// -/// * [showDialogWithFadeTransition], which displays the dialog popup. +/// * [showModalWithFadeTransition], which displays the modal popup. class FadeModalRoute extends PopupRoute { /// Creates a [FadeModalRoute] route with the Material fade transition. /// From b28aaf58723042724468c96a7b0689ff1a6f0cdc Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Fri, 10 Jan 2020 09:34:12 -0800 Subject: [PATCH 08/29] Minor modifications to FlutterLogoModal --- .../animations/lib/src/fade_transition.dart | 29 ++++++++------- .../animations/test/fade_transition_test.dart | 35 +++++++++++-------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index d8639e1b48f0..a1a77f940103 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -35,22 +35,27 @@ import 'package:flutter/material.dart'; /// /// /// Displays a modal with the FlutterLogo on it. /// class FlutterLogoModal extends StatelessWidget { -/// FlutterLogoModal(); +/// const _FlutterLogoModal(); /// /// @override /// Widget build(BuildContext context) { -/// return Center( -/// child: ConstrainedBox( -/// constraints: const BoxConstraints( -/// maxHeight: 500, -/// maxWidth: 500, -/// minHeight: 250, -/// minWidth: 250, +/// return Column( +/// mainAxisAlignment: MainAxisAlignment.center, +/// children: [ +/// Center( +/// child: ConstrainedBox( +/// constraints: const BoxConstraints( +/// maxHeight: 300, +/// maxWidth: 300, +/// minHeight: 250, +/// minWidth: 250, +/// ), +/// child: const Material( +/// child: Center(child: FlutterLogo(size: 250)), +/// ), +/// ), /// ), -/// child: const Material( -/// child: Center(child: FlutterLogo(size: 250)), -/// ), -/// ), +/// ], /// ); /// } /// } diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index ab6e83c23f5c..8b4f6652e346 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -21,7 +21,7 @@ void main() { onPressed: () { showModalWithFadeTransition( context: context, - child: const _FlutterLogoDialog(), + child: const _FlutterLogoModal(), ); }, child: Icon(Icons.add), @@ -34,28 +34,33 @@ void main() { ); await tester.tap(find.byType(RaisedButton)); await tester.pumpAndSettle(); - expect(find.byType(_FlutterLogoDialog), findsOneWidget); + expect(find.byType(_FlutterLogoModal), findsOneWidget); }, ); } -class _FlutterLogoDialog extends StatelessWidget { - const _FlutterLogoDialog(); +class _FlutterLogoModal extends StatelessWidget { + const _FlutterLogoModal(); @override Widget build(BuildContext context) { - return Center( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 500, - maxWidth: 500, - minHeight: 250, - minWidth: 250, - ), - child: const Material( - child: Center(child: FlutterLogo(size: 250)), + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 300, + maxWidth: 300, + minHeight: 250, + minWidth: 250, + ), + child: const Material( + child: Center(child: FlutterLogo(size: 250)), + ), + ), ), - ), + ], ); } } From 88670c2339037f5af8e38dfe3134a963e56f25aa Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Fri, 10 Jan 2020 09:43:37 -0800 Subject: [PATCH 09/29] Make Dart formatter happy --- .../animations/lib/src/fade_transition.dart | 44 ++++++++++++------- .../animations/test/fade_transition_test.dart | 28 ++++++------ 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index a1a77f940103..74f3c450741a 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -95,14 +95,17 @@ Future showModalWithFadeTransition({ bool useRootNavigator = true, Widget child, }) { - barrierLabel = barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel; + barrierLabel = barrierLabel ?? + MaterialLocalizations.of(context).modalBarrierDismissLabel; assert(useRootNavigator != null); assert(!barrierDismissible || barrierLabel != null); - return Navigator.of(context, rootNavigator: useRootNavigator).push(FadeModalRoute( - barrierDismissible: barrierDismissible, - barrierLabel: barrierLabel, - child: child, - )); + return Navigator.of(context, rootNavigator: useRootNavigator).push( + FadeModalRoute( + barrierDismissible: barrierDismissible, + barrierLabel: barrierLabel, + child: child, + ), + ); } /// A modal route that overlays a widget on the current route with the Material @@ -124,9 +127,9 @@ class FadeModalRoute extends PopupRoute { bool barrierDismissible = true, String barrierLabel, @required this.child, - }) : assert(barrierDismissible != null), - _barrierDismissible = barrierDismissible, - _barrierLabel = barrierLabel; + }) : assert(barrierDismissible != null), + _barrierDismissible = barrierDismissible, + _barrierLabel = barrierLabel; @override bool get barrierDismissible => _barrierDismissible; @@ -149,16 +152,18 @@ class FadeModalRoute extends PopupRoute { final Widget child; @override - Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { final ThemeData theme = Theme.of(context); return Semantics( child: SafeArea( child: Builder( builder: (BuildContext context) { - return theme != null - ? Theme(data: theme, child: child) - : child; - } + return theme != null ? Theme(data: theme, child: child) : child; + }, ), ), scopesRoute: true, @@ -167,7 +172,12 @@ class FadeModalRoute extends PopupRoute { } @override - Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget child) { @@ -201,7 +211,9 @@ class _EnterTransition extends StatelessWidget { final Animation animation; final Widget child; - static Animatable fadeInTransition = CurveTween(curve: const Interval(0.0, 0.3)); + static Animatable fadeInTransition = CurveTween( + curve: const Interval(0.0, 0.3), + ); static Animatable scaleInTransition = Tween( begin: 0.80, end: 1.00, diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index 8b4f6652e346..9184eb55edbe 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -14,21 +14,19 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: Builder( - builder: (BuildContext context) { - return Center( - child: RaisedButton( - onPressed: () { - showModalWithFadeTransition( - context: context, - child: const _FlutterLogoModal(), - ); - }, - child: Icon(Icons.add), - ), - ); - } - ), + body: Builder(builder: (BuildContext context) { + return Center( + child: RaisedButton( + onPressed: () { + showModalWithFadeTransition( + context: context, + child: const _FlutterLogoModal(), + ); + }, + child: Icon(Icons.add), + ), + ); + }), ), ), ); From e8a45304f9b601239b0794b1d093c30b05294179 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Mon, 13 Jan 2020 13:35:42 -0800 Subject: [PATCH 10/29] WIP - generalize showModal function --- .../animations/lib/src/fade_transition.dart | 221 +++-------------- packages/animations/lib/src/utils/modal.dart | 231 ++++++++++++++++++ .../animations/test/fade_transition_test.dart | 8 +- 3 files changed, 275 insertions(+), 185 deletions(-) create mode 100644 packages/animations/lib/src/utils/modal.dart diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index 74f3c450741a..cade6033795e 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -2,142 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:animations/src/utils/curves.dart'; import 'package:flutter/material.dart'; +import 'utils/curves.dart'; +import 'utils/modal.dart'; -/// Displays a modal above the current contents of the app. -/// -/// This function displays the [FadeModalRoute], which transitions in -/// with the Material fade transition. -/// -/// Content below the modal is dimmed with a [ModalBarrier]. -/// -/// ```dart -/// /// Sample widget that uses [showModalWithFadeTransition]. -/// class MyHomePage extends StatelessWidget { -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// body: Center( -/// child: RaisedButton( -/// onPressed: () { -/// showModalWithFadeTransition( -/// context: context, -/// child: FlutterLogoModal(), -/// ); -/// }, -/// child: Icon(Icons.add), -/// ), -/// ), -/// ); -/// } -/// } -/// -/// /// Displays a modal with the FlutterLogo on it. -/// class FlutterLogoModal extends StatelessWidget { -/// const _FlutterLogoModal(); -/// -/// @override -/// Widget build(BuildContext context) { -/// return Column( -/// mainAxisAlignment: MainAxisAlignment.center, -/// children: [ -/// Center( -/// child: ConstrainedBox( -/// constraints: const BoxConstraints( -/// maxHeight: 300, -/// maxWidth: 300, -/// minHeight: 250, -/// minWidth: 250, -/// ), -/// child: const Material( -/// child: Center(child: FlutterLogo(size: 250)), -/// ), -/// ), -/// ), -/// ], -/// ); -/// } -/// } -/// ``` -/// -/// The `context` argument is used to look up the [Navigator] for the -/// modal. It is only used when the method is called. Its corresponding widget -/// can be safely removed from the tree before the modal is closed. -/// -/// The `useRootNavigator` argument is used to determine whether to push the -/// modal to the [Navigator] furthest from or nearest to the given `context`. -/// By default, `useRootNavigator` is `true` and the modal route created by -/// this method is pushed to the root navigator. -/// -/// If the application has multiple [Navigator] objects, it may be necessary to -/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the -/// modal rather than just `Navigator.pop(context, result)`. -/// -/// The `barrierDismissible` argument is used to determine whether this route -/// can be dismissed by tapping the modal barrier. This argument defaults -/// to true. If `barrierDismissible` is true, a non-null `barrierLabel` must be -/// provided. -/// -/// The `barrierLabel` argument is the semantic label used for a dismissible -/// barrier. This argument defaults to "Dismiss". -/// -/// Returns a [Future] that resolves to the value (if any) that was passed to -/// [Navigator.pop] when the modal was closed. -/// -/// See also: -/// -/// * [FadeModalRoute], which is the route that is built by this function. -Future showModalWithFadeTransition({ - @required BuildContext context, - bool barrierDismissible = true, - String barrierLabel, - bool useRootNavigator = true, - Widget child, -}) { - barrierLabel = barrierLabel ?? - MaterialLocalizations.of(context).modalBarrierDismissLabel; - assert(useRootNavigator != null); - assert(!barrierDismissible || barrierLabel != null); - return Navigator.of(context, rootNavigator: useRootNavigator).push( - FadeModalRoute( - barrierDismissible: barrierDismissible, - barrierLabel: barrierLabel, - child: child, - ), - ); -} - -/// A modal route that overlays a widget on the current route with the Material -/// fade transition. -/// -/// The fade pattern is used for UI elements that enter or exit from within -/// the screen bounds. Elements that enter use a quick fade in and scale from -/// 80% to 100%. Elements that exit simply fade out. The scale animation is -/// only applied to entering elements to emphasize new content over old. -/// -/// See also: -/// -/// * [showModalWithFadeTransition], which displays the modal popup. -class FadeModalRoute extends PopupRoute { - /// Creates a [FadeModalRoute] route with the Material fade transition. - /// - /// [barrierDismissible] is true by default. - FadeModalRoute({ +class FadeTransitionConfiguration extends ModalConfiguration { + FadeTransitionConfiguration({ bool barrierDismissible = true, String barrierLabel, - @required this.child, - }) : assert(barrierDismissible != null), - _barrierDismissible = barrierDismissible, - _barrierLabel = barrierLabel; - - @override - bool get barrierDismissible => _barrierDismissible; - final bool _barrierDismissible; - - @override - String get barrierLabel => _barrierLabel; - final String _barrierLabel; + }) : super( + barrierDismissible: barrierDismissible, + barrierLabel: barrierLabel, + ); @override Color get barrierColor => Colors.black54; @@ -148,60 +24,39 @@ class FadeModalRoute extends PopupRoute { @override Duration get reverseTransitionDuration => const Duration(milliseconds: 75); - /// The primary contents of the modal. - final Widget child; - @override - Widget buildPage( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - ) { - final ThemeData theme = Theme.of(context); - return Semantics( - child: SafeArea( - child: Builder( - builder: (BuildContext context) { - return theme != null ? Theme(data: theme, child: child) : child; - }, - ), - ), - scopesRoute: true, - explicitChildNodes: true, - ); - } - - @override - Widget buildTransitions( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, - ) { - return AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget child) { - switch (animation.status) { - case AnimationStatus.forward: - return _EnterTransition( - animation: animation, - child: child, - ); - case AnimationStatus.dismissed: - case AnimationStatus.reverse: - case AnimationStatus.completed: - return FadeTransition( - opacity: animation, - child: child, - ); - } - return null; // unreachable - }, - child: child, - ); - } + ModalTransitionBuilder get transitionBuilder => _modalFadeTransitionBuilder; } +ModalTransitionBuilder _modalFadeTransitionBuilder = ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, +) { + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget child) { + switch (animation.status) { + case AnimationStatus.forward: + return _EnterTransition( + animation: animation, + child: child, + ); + case AnimationStatus.dismissed: + case AnimationStatus.reverse: + case AnimationStatus.completed: + return FadeTransition( + opacity: animation, + child: child, + ); + } + return null; // unreachable + }, + child: child, + ); +}; + class _EnterTransition extends StatelessWidget { const _EnterTransition({ this.animation, diff --git a/packages/animations/lib/src/utils/modal.dart b/packages/animations/lib/src/utils/modal.dart new file mode 100644 index 000000000000..7e2bd30056bf --- /dev/null +++ b/packages/animations/lib/src/utils/modal.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; + +/// Signature for a function that creates a widget that builds a +/// transition. +/// +/// Used by [PopupRoute.buildTransitions]. +typedef ModalTransitionBuilder = Widget Function( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, +); + +/// Displays a modal above the current contents of the app. +/// +/// This function displays the [FadeModalRoute], which transitions in +/// with the Material fade transition. +/// +/// Content below the modal is dimmed with a [ModalBarrier]. +/// +/// ```dart +/// /// Sample widget that uses [showModalWithFadeTransition]. +/// class MyHomePage extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: Center( +/// child: RaisedButton( +/// onPressed: () { +/// showModalWithFadeTransition( +/// context: context, +/// builder: (BuildContext context) { +/// return CenteredFlutterLogo(), +/// }, +/// ); +/// }, +/// child: Icon(Icons.add), +/// ), +/// ), +/// ); +/// } +/// } +/// +/// /// Displays a centered Flutter logo with size constraints. +/// class CenteredFlutterLogo extends StatelessWidget { +/// const _CenteredFlutterLogo(); +/// +/// @override +/// Widget build(BuildContext context) { +/// return Column( +/// mainAxisAlignment: MainAxisAlignment.center, +/// children: [ +/// Center( +/// child: ConstrainedBox( +/// constraints: const BoxConstraints( +/// maxHeight: 300, +/// maxWidth: 300, +/// minHeight: 250, +/// minWidth: 250, +/// ), +/// child: const Material( +/// child: Center(child: FlutterLogo(size: 250)), +/// ), +/// ), +/// ), +/// ], +/// ); +/// } +/// } +/// ``` +/// +/// The `context` argument is used to look up the [Navigator] for the +/// modal. It is only used when the method is called. Its corresponding widget +/// can be safely removed from the tree before the modal is closed. +/// +/// The `useRootNavigator` argument is used to determine whether to push the +/// modal to the [Navigator] furthest from or nearest to the given `context`. +/// By default, `useRootNavigator` is `true` and the modal route created by +/// this method is pushed to the root navigator. If the application has +/// multiple [Navigator] objects, it may be necessary to call +/// `Navigator.of(context, rootNavigator: true).pop(result)` to close the +/// modal rather than just `Navigator.pop(context, result)`. +/// +/// The `barrierDismissible` argument is used to determine whether this route +/// can be dismissed by tapping the modal barrier. This argument defaults +/// to true. If `barrierDismissible` is true, a non-null `barrierLabel` must be +/// provided. +/// +/// The `barrierLabel` argument is the semantic label used for a dismissible +/// barrier. This argument defaults to "Dismiss". +/// +/// Returns a [Future] that resolves to the value (if any) that was passed to +/// [Navigator.pop] when the modal was closed. +/// +/// See also: +/// +/// * [FadeModalRoute], which is the route that is built by this function. +Future showModal({ + @required BuildContext context, + ModalConfiguration configuration, + bool useRootNavigator = true, + WidgetBuilder builder, +}) { + String barrierLabel = configuration.barrierLabel; + // Avoid looking up [MaterialLocalizations.of(context).modalBarrierDismissLabel] + // if there is no dismissible barrier. + if (configuration.barrierDismissible == true && configuration.barrierLabel == null) { + barrierLabel = MaterialLocalizations.of(context).modalBarrierDismissLabel; + } + assert(useRootNavigator != null); + assert(!configuration.barrierDismissible || barrierLabel != null); + return Navigator.of(context, rootNavigator: useRootNavigator).push( + _ModalRoute( + barrierColor: configuration.barrierColor, + barrierDismissible: configuration.barrierDismissible, + barrierLabel: barrierLabel, + transitionBuilder: configuration.transitionBuilder, + transitionDuration: configuration.transitionDuration, + reverseTransitionDuration: configuration.reverseTransitionDuration, + builder: builder, + ), + ); +} + +/// A modal route that overlays a widget on the current route with the Material +/// fade transition. +/// +/// The fade pattern is used for UI elements that enter or exit from within +/// the screen bounds. Elements that enter use a quick fade in and scale from +/// 80% to 100%. Elements that exit simply fade out. The scale animation is +/// only applied to entering elements to emphasize new content over old. +/// +/// See also: +/// +/// * [showModalWithFadeTransition], which displays the modal popup. +class _ModalRoute extends PopupRoute { + /// Creates a [_ModalRoute] route with the Material fade transition. + /// + /// [barrierDismissible] is true by default. + _ModalRoute({ + Color barrierColor, + bool barrierDismissible = true, + String barrierLabel, + ModalTransitionBuilder transitionBuilder, + Duration transitionDuration, + Duration reverseTransitionDuration, + @required this.builder, + }) : assert(barrierDismissible != null), + _barrierColor = barrierColor, + _barrierDismissible = barrierDismissible, + _barrierLabel = barrierLabel, + _transitionBuilder = transitionBuilder, + _transitionDuration = transitionDuration, + _reverseTransitionDuration = reverseTransitionDuration; + + @override + Color get barrierColor => _barrierColor; + final Color _barrierColor; + + @override + bool get barrierDismissible => _barrierDismissible; + final bool _barrierDismissible; + + @override + String get barrierLabel => _barrierLabel; + final String _barrierLabel; + + @override + Duration get transitionDuration => _transitionDuration; + final Duration _transitionDuration; + + @override + Duration get reverseTransitionDuration => _reverseTransitionDuration; + final Duration _reverseTransitionDuration; + + /// The primary contents of the modal. + final WidgetBuilder builder; + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + final ThemeData theme = Theme.of(context); + return Semantics( + child: SafeArea( + child: Builder( + builder: (BuildContext context) { + final Widget child = Builder(builder: builder); + return theme != null ? Theme(data: theme, child: child) : child; + }, + ), + ), + scopesRoute: true, + explicitChildNodes: true, + ); + } + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) => _transitionBuilder(context, animation, secondaryAnimation, child); + final ModalTransitionBuilder _transitionBuilder; +} + +abstract class ModalConfiguration { + ModalConfiguration({ + this.barrierColor, + this.barrierDismissible, + this.barrierLabel, + this.transitionBuilder, + this.transitionDuration, + this.reverseTransitionDuration, + }); + + final Color barrierColor; + + final bool barrierDismissible; + + final String barrierLabel; + + final ModalTransitionBuilder transitionBuilder; + + final Duration transitionDuration; + + final Duration reverseTransitionDuration; +} diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index 9184eb55edbe..c95321c1d1d9 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:animations/src/fade_transition.dart'; +import 'package:animations/src/utils/modal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; @@ -18,9 +19,12 @@ void main() { return Center( child: RaisedButton( onPressed: () { - showModalWithFadeTransition( + showModal( context: context, - child: const _FlutterLogoModal(), + configuration: FadeTransitionConfiguration(), + builder: (BuildContext context) { + return const _FlutterLogoModal(); + }, ); }, child: Icon(Icons.add), From b3030eb1d8fcba3102920bdcf4cf8b3140d67116 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Mon, 13 Jan 2020 14:08:39 -0800 Subject: [PATCH 11/29] API docs and code cleanup --- .../animations/lib/src/fade_transition.dart | 61 +++++++++- packages/animations/lib/src/utils/modal.dart | 108 ++++++------------ 2 files changed, 92 insertions(+), 77 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index cade6033795e..f844eacda0f4 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -6,7 +6,66 @@ import 'package:flutter/material.dart'; import 'utils/curves.dart'; import 'utils/modal.dart'; +/// The modal transition configuration for a Material fade transition. +/// +/// The fade pattern is used for UI elements that enter or exit from within +/// the screen bounds. Elements that enter use a quick fade in and scale from +/// 80% to 100%. Elements that exit simply fade out. The scale animation is +/// only applied to entering elements to emphasize new content over old. +/// +/// ```dart +/// /// Sample widget that uses [showModal] with [FadeTransitionConfiguration]. +/// class MyHomePage extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: Center( +/// child: RaisedButton( +/// onPressed: () { +/// showModal( +/// context: context, +/// configuration: FadeTransitionConfiguration(), +/// builder: (BuildContext context) { +/// return CenteredFlutterLogo(), +/// }, +/// ); +/// }, +/// child: Icon(Icons.add), +/// ), +/// ), +/// ); +/// } +/// } +/// +/// /// Displays a centered Flutter logo with size constraints. +/// class CenteredFlutterLogo extends StatelessWidget { +/// const _CenteredFlutterLogo(); +/// +/// @override +/// Widget build(BuildContext context) { +/// return Column( +/// mainAxisAlignment: MainAxisAlignment.center, +/// children: [ +/// Center( +/// child: ConstrainedBox( +/// constraints: const BoxConstraints( +/// maxHeight: 300, +/// maxWidth: 300, +/// minHeight: 250, +/// minWidth: 250, +/// ), +/// child: const Material( +/// child: Center(child: FlutterLogo(size: 250)), +/// ), +/// ), +/// ), +/// ], +/// ); +/// } +/// } +/// ``` class FadeTransitionConfiguration extends ModalConfiguration { + /// Creates the Material fade transition configuration. FadeTransitionConfiguration({ bool barrierDismissible = true, String barrierLabel, @@ -28,7 +87,7 @@ class FadeTransitionConfiguration extends ModalConfiguration { ModalTransitionBuilder get transitionBuilder => _modalFadeTransitionBuilder; } -ModalTransitionBuilder _modalFadeTransitionBuilder = ( +final ModalTransitionBuilder _modalFadeTransitionBuilder = ( BuildContext context, Animation animation, Animation secondaryAnimation, diff --git a/packages/animations/lib/src/utils/modal.dart b/packages/animations/lib/src/utils/modal.dart index 7e2bd30056bf..e51def84d882 100644 --- a/packages/animations/lib/src/utils/modal.dart +++ b/packages/animations/lib/src/utils/modal.dart @@ -13,62 +13,8 @@ typedef ModalTransitionBuilder = Widget Function( /// Displays a modal above the current contents of the app. /// -/// This function displays the [FadeModalRoute], which transitions in -/// with the Material fade transition. -/// /// Content below the modal is dimmed with a [ModalBarrier]. /// -/// ```dart -/// /// Sample widget that uses [showModalWithFadeTransition]. -/// class MyHomePage extends StatelessWidget { -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// body: Center( -/// child: RaisedButton( -/// onPressed: () { -/// showModalWithFadeTransition( -/// context: context, -/// builder: (BuildContext context) { -/// return CenteredFlutterLogo(), -/// }, -/// ); -/// }, -/// child: Icon(Icons.add), -/// ), -/// ), -/// ); -/// } -/// } -/// -/// /// Displays a centered Flutter logo with size constraints. -/// class CenteredFlutterLogo extends StatelessWidget { -/// const _CenteredFlutterLogo(); -/// -/// @override -/// Widget build(BuildContext context) { -/// return Column( -/// mainAxisAlignment: MainAxisAlignment.center, -/// children: [ -/// Center( -/// child: ConstrainedBox( -/// constraints: const BoxConstraints( -/// maxHeight: 300, -/// maxWidth: 300, -/// minHeight: 250, -/// minWidth: 250, -/// ), -/// child: const Material( -/// child: Center(child: FlutterLogo(size: 250)), -/// ), -/// ), -/// ), -/// ], -/// ); -/// } -/// } -/// ``` -/// /// The `context` argument is used to look up the [Navigator] for the /// modal. It is only used when the method is called. Its corresponding widget /// can be safely removed from the tree before the modal is closed. @@ -81,33 +27,27 @@ typedef ModalTransitionBuilder = Widget Function( /// `Navigator.of(context, rootNavigator: true).pop(result)` to close the /// modal rather than just `Navigator.pop(context, result)`. /// -/// The `barrierDismissible` argument is used to determine whether this route -/// can be dismissed by tapping the modal barrier. This argument defaults -/// to true. If `barrierDismissible` is true, a non-null `barrierLabel` must be -/// provided. -/// -/// The `barrierLabel` argument is the semantic label used for a dismissible -/// barrier. This argument defaults to "Dismiss". -/// /// Returns a [Future] that resolves to the value (if any) that was passed to /// [Navigator.pop] when the modal was closed. /// /// See also: /// -/// * [FadeModalRoute], which is the route that is built by this function. +/// * [ModalConfiguration], which is the configuration object used to define +/// the modal's characteristics. Future showModal({ @required BuildContext context, - ModalConfiguration configuration, + @required ModalConfiguration configuration, bool useRootNavigator = true, WidgetBuilder builder, }) { + assert(configuration != null); + assert(useRootNavigator != null); String barrierLabel = configuration.barrierLabel; // Avoid looking up [MaterialLocalizations.of(context).modalBarrierDismissLabel] // if there is no dismissible barrier. if (configuration.barrierDismissible == true && configuration.barrierLabel == null) { barrierLabel = MaterialLocalizations.of(context).modalBarrierDismissLabel; } - assert(useRootNavigator != null); assert(!configuration.barrierDismissible || barrierLabel != null); return Navigator.of(context, rootNavigator: useRootNavigator).push( _ModalRoute( @@ -122,17 +62,7 @@ Future showModal({ ); } -/// A modal route that overlays a widget on the current route with the Material -/// fade transition. -/// -/// The fade pattern is used for UI elements that enter or exit from within -/// the screen bounds. Elements that enter use a quick fade in and scale from -/// 80% to 100%. Elements that exit simply fade out. The scale animation is -/// only applied to entering elements to emphasize new content over old. -/// -/// See also: -/// -/// * [showModalWithFadeTransition], which displays the modal popup. +// A modal route that overlays a widget on the current route. class _ModalRoute extends PopupRoute { /// Creates a [_ModalRoute] route with the Material fade transition. /// @@ -207,7 +137,19 @@ class _ModalRoute extends PopupRoute { final ModalTransitionBuilder _transitionBuilder; } +/// A configuration object containing the properties needed to implement a +/// modal route. +/// +/// The `barrierDismissible` argument is used to determine whether this route +/// can be dismissed by tapping the modal barrier. This argument defaults +/// to true. If `barrierDismissible` is true, a non-null `barrierLabel` must be +/// provided. +/// +/// The `barrierLabel` argument is the semantic label used for a dismissible +/// barrier. This argument defaults to "Dismiss". abstract class ModalConfiguration { + /// Creates a modal configuration object that provides the necessary + /// properties to implement a modal route. ModalConfiguration({ this.barrierColor, this.barrierDismissible, @@ -217,15 +159,29 @@ abstract class ModalConfiguration { this.reverseTransitionDuration, }); + /// The color to use for the modal barrier. If this is null, the barrier will + /// be transparent. final Color barrierColor; + /// Whether you can dismiss this route by tapping the modal barrier. final bool barrierDismissible; + /// The semantic label used for a dismissible barrier. final String barrierLabel; + /// A builder that defines how the route arrives on and leaves the screen. + /// + /// The [buildTransitions] method is typically used to define transitions + /// that animate the new topmost route's comings and goings. When the + /// [Navigator] pushes a route on the top of its stack, the new route's + /// primary [animation] runs from 0.0 to 1.0. When the [Navigator] pops the + /// topmost route, e.g. because the use pressed the back button, the + /// primary animation runs from 1.0 to 0.0. final ModalTransitionBuilder transitionBuilder; + /// The duration of the transition running forwards. final Duration transitionDuration; + /// The duration of the transition running in reverse. final Duration reverseTransitionDuration; } From 7ff4a0221b0c22df2247b6b72f55dae68a7bbdfc Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Mon, 13 Jan 2020 16:29:24 -0800 Subject: [PATCH 12/29] Address code review feedback --- packages/animations/lib/animations.dart | 1 + .../animations/lib/src/fade_transition.dart | 70 +++++++++---------- .../animations/lib/src/{utils => }/modal.dart | 18 +++-- .../animations/test/fade_transition_test.dart | 25 +++---- 4 files changed, 59 insertions(+), 55 deletions(-) rename packages/animations/lib/src/{utils => }/modal.dart (92%) diff --git a/packages/animations/lib/animations.dart b/packages/animations/lib/animations.dart index a87cbde9e2ea..731e03e23574 100644 --- a/packages/animations/lib/animations.dart +++ b/packages/animations/lib/animations.dart @@ -4,6 +4,7 @@ export 'src/fade_through_transition.dart'; export 'src/fade_transition.dart'; +export 'src/modal.dart'; export 'src/open_container.dart'; export 'src/page_transition_switcher.dart'; export 'src/shared_z_axis_transition.dart'; diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index f844eacda0f4..773452483390 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -3,8 +3,9 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; + +import 'modal.dart'; import 'utils/curves.dart'; -import 'utils/modal.dart'; /// The modal transition configuration for a Material fade transition. /// @@ -69,10 +70,11 @@ class FadeTransitionConfiguration extends ModalConfiguration { FadeTransitionConfiguration({ bool barrierDismissible = true, String barrierLabel, - }) : super( - barrierDismissible: barrierDismissible, - barrierLabel: barrierLabel, - ); + }) : assert(barrierDismissible != null), + super( + barrierDismissible: barrierDismissible, + barrierLabel: barrierLabel, + ); @override Color get barrierColor => Colors.black54; @@ -84,38 +86,36 @@ class FadeTransitionConfiguration extends ModalConfiguration { Duration get reverseTransitionDuration => const Duration(milliseconds: 75); @override - ModalTransitionBuilder get transitionBuilder => _modalFadeTransitionBuilder; + Widget transitionBuilder( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget child) { + switch (animation.status) { + case AnimationStatus.forward: + return _EnterTransition( + animation: animation, + child: child, + ); + case AnimationStatus.dismissed: + case AnimationStatus.reverse: + case AnimationStatus.completed: + return FadeTransition( + opacity: animation, + child: child, + ); + } + return null; // unreachable + }, + child: child, + ); + } } -final ModalTransitionBuilder _modalFadeTransitionBuilder = ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, -) { - return AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget child) { - switch (animation.status) { - case AnimationStatus.forward: - return _EnterTransition( - animation: animation, - child: child, - ); - case AnimationStatus.dismissed: - case AnimationStatus.reverse: - case AnimationStatus.completed: - return FadeTransition( - opacity: animation, - child: child, - ); - } - return null; // unreachable - }, - child: child, - ); -}; - class _EnterTransition extends StatelessWidget { const _EnterTransition({ this.animation, diff --git a/packages/animations/lib/src/utils/modal.dart b/packages/animations/lib/src/modal.dart similarity index 92% rename from packages/animations/lib/src/utils/modal.dart rename to packages/animations/lib/src/modal.dart index e51def84d882..d530aaad0343 100644 --- a/packages/animations/lib/src/utils/modal.dart +++ b/packages/animations/lib/src/modal.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; /// Signature for a function that creates a widget that builds a /// transition. /// -/// Used by [PopupRoute.buildTransitions]. +/// Used by [PopupRoute]. typedef ModalTransitionBuilder = Widget Function( BuildContext context, Animation animation, @@ -19,6 +19,11 @@ typedef ModalTransitionBuilder = Widget Function( /// modal. It is only used when the method is called. Its corresponding widget /// can be safely removed from the tree before the modal is closed. /// +/// The `configuration` argument is used to determine characteristics of the +/// modal route that will be displayed, such as the enter and exit +/// transitions, the duration of the transitions, and modal barrier +/// properties. +/// /// The `useRootNavigator` argument is used to determine whether to push the /// modal to the [Navigator] furthest from or nearest to the given `context`. /// By default, `useRootNavigator` is `true` and the modal route created by @@ -45,7 +50,7 @@ Future showModal({ String barrierLabel = configuration.barrierLabel; // Avoid looking up [MaterialLocalizations.of(context).modalBarrierDismissLabel] // if there is no dismissible barrier. - if (configuration.barrierDismissible == true && configuration.barrierLabel == null) { + if (configuration.barrierDismissible && configuration.barrierLabel == null) { barrierLabel = MaterialLocalizations.of(context).modalBarrierDismissLabel; } assert(!configuration.barrierDismissible || barrierLabel != null); @@ -134,6 +139,7 @@ class _ModalRoute extends PopupRoute { Animation secondaryAnimation, Widget child, ) => _transitionBuilder(context, animation, secondaryAnimation, child); + final ModalTransitionBuilder _transitionBuilder; } @@ -154,7 +160,6 @@ abstract class ModalConfiguration { this.barrierColor, this.barrierDismissible, this.barrierLabel, - this.transitionBuilder, this.transitionDuration, this.reverseTransitionDuration, }); @@ -177,7 +182,12 @@ abstract class ModalConfiguration { /// primary [animation] runs from 0.0 to 1.0. When the [Navigator] pops the /// topmost route, e.g. because the use pressed the back button, the /// primary animation runs from 1.0 to 0.0. - final ModalTransitionBuilder transitionBuilder; + Widget transitionBuilder( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ); /// The duration of the transition running forwards. final Duration transitionDuration; diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index c95321c1d1d9..92f22e82954f 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:animations/src/fade_transition.dart'; -import 'package:animations/src/utils/modal.dart'; +import 'package:animations/src/modal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; @@ -46,23 +46,16 @@ class _FlutterLogoModal extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 300, - maxWidth: 300, - minHeight: 250, - minWidth: 250, - ), - child: const Material( - child: Center(child: FlutterLogo(size: 250)), - ), + return const Center( + child: SizedBox( + width: 250, + height: 250, + child: Material( + child: Center( + child: FlutterLogo(size: 250), ), ), - ], + ), ); } } From fd4b5457db1c92cc189c49289c8bd3fd0a1fc3eb Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Mon, 13 Jan 2020 16:31:29 -0800 Subject: [PATCH 13/29] Fix doc to use SizedBox instead of Column and ConstrainedBox --- .../animations/lib/src/fade_transition.dart | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index 773452483390..05e9d87aca40 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -27,7 +27,7 @@ import 'utils/curves.dart'; /// context: context, /// configuration: FadeTransitionConfiguration(), /// builder: (BuildContext context) { -/// return CenteredFlutterLogo(), +/// return _CenteredFlutterLogo(), /// }, /// ); /// }, @@ -39,28 +39,21 @@ import 'utils/curves.dart'; /// } /// /// /// Displays a centered Flutter logo with size constraints. -/// class CenteredFlutterLogo extends StatelessWidget { +/// class _CenteredFlutterLogo extends StatelessWidget { /// const _CenteredFlutterLogo(); /// /// @override /// Widget build(BuildContext context) { -/// return Column( -/// mainAxisAlignment: MainAxisAlignment.center, -/// children: [ -/// Center( -/// child: ConstrainedBox( -/// constraints: const BoxConstraints( -/// maxHeight: 300, -/// maxWidth: 300, -/// minHeight: 250, -/// minWidth: 250, -/// ), -/// child: const Material( -/// child: Center(child: FlutterLogo(size: 250)), -/// ), +/// return Center( +/// child: SizedBox( +/// width: 250, +/// height: 250, +/// child: const Material( +/// child: Center( +/// child: FlutterLogo(size: 250), /// ), /// ), -/// ], +/// ), /// ); /// } /// } From b23ca9ad358ce952ecc58558cc441cacfcfb7bc0 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Mon, 13 Jan 2020 16:32:23 -0800 Subject: [PATCH 14/29] Remove erroneous comma and replace with semicolon --- packages/animations/lib/src/fade_transition.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index 05e9d87aca40..e202d55b0627 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -27,7 +27,7 @@ import 'utils/curves.dart'; /// context: context, /// configuration: FadeTransitionConfiguration(), /// builder: (BuildContext context) { -/// return _CenteredFlutterLogo(), +/// return _CenteredFlutterLogo(); /// }, /// ); /// }, From b16e820da022a8b4ddfdf333e1ebd811e9c16707 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Mon, 13 Jan 2020 16:42:56 -0800 Subject: [PATCH 15/29] Make formatter happy --- packages/animations/lib/src/modal.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/animations/lib/src/modal.dart b/packages/animations/lib/src/modal.dart index d530aaad0343..d7cc370edb02 100644 --- a/packages/animations/lib/src/modal.dart +++ b/packages/animations/lib/src/modal.dart @@ -138,7 +138,14 @@ class _ModalRoute extends PopupRoute { Animation animation, Animation secondaryAnimation, Widget child, - ) => _transitionBuilder(context, animation, secondaryAnimation, child); + ) { + return _transitionBuilder( + context, + animation, + secondaryAnimation, + child, + ); + } final ModalTransitionBuilder _transitionBuilder; } From 733084bb2671c3bd20542bfc4a4b45c7e4bacae4 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Mon, 13 Jan 2020 17:22:06 -0800 Subject: [PATCH 16/29] WIP - test scaffolding --- packages/animations/lib/src/modal.dart | 10 +- .../animations/test/fade_transition_test.dart | 10 +- packages/animations/test/modal_test.dart | 102 ++++++++++++++++++ 3 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 packages/animations/test/modal_test.dart diff --git a/packages/animations/lib/src/modal.dart b/packages/animations/lib/src/modal.dart index d7cc370edb02..438267e1f957 100644 --- a/packages/animations/lib/src/modal.dart +++ b/packages/animations/lib/src/modal.dart @@ -1,10 +1,14 @@ +// Copyright 2019 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'; /// Signature for a function that creates a widget that builds a /// transition. /// /// Used by [PopupRoute]. -typedef ModalTransitionBuilder = Widget Function( +typedef _ModalTransitionBuilder = Widget Function( BuildContext context, Animation animation, Animation secondaryAnimation, @@ -76,7 +80,7 @@ class _ModalRoute extends PopupRoute { Color barrierColor, bool barrierDismissible = true, String barrierLabel, - ModalTransitionBuilder transitionBuilder, + _ModalTransitionBuilder transitionBuilder, Duration transitionDuration, Duration reverseTransitionDuration, @required this.builder, @@ -147,7 +151,7 @@ class _ModalRoute extends PopupRoute { ); } - final ModalTransitionBuilder _transitionBuilder; + final _ModalTransitionBuilder _transitionBuilder; } /// A configuration object containing the properties needed to implement a diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index 92f22e82954f..239e29082d3f 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -10,7 +10,7 @@ import 'package:flutter/widgets.dart'; void main() { testWidgets( - 'showModalWithFadeTransition builds a new route', + 'FadeTransitionConfiguration builds a new route', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -39,6 +39,14 @@ void main() { expect(find.byType(_FlutterLogoModal), findsOneWidget); }, ); + + // runs forward + + // runs backwards + + // does not get interrupted when run in reverse + + // state is not lost when transitioning } class _FlutterLogoModal extends StatelessWidget { diff --git a/packages/animations/test/modal_test.dart b/packages/animations/test/modal_test.dart new file mode 100644 index 000000000000..ce9cf2daa76e --- /dev/null +++ b/packages/animations/test/modal_test.dart @@ -0,0 +1,102 @@ +// Copyright 2019 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:animations/src/modal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + testWidgets( + 'showModal builds a new route', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: RaisedButton( + onPressed: () { + showModal( + context: context, + configuration: _TestModalConfiguration(), + builder: (BuildContext context) { + return const _FlutterLogoModal(); + }, + ); + }, + child: Icon(Icons.add), + ), + ); + }), + ), + ), + ); + await tester.tap(find.byType(RaisedButton)); + await tester.pumpAndSettle(); + expect(find.byType(_FlutterLogoModal), findsOneWidget); + }, + ); + + // runs forward + + // runs backwards + + // does not get interrupted when run in reverse + + // state is not lost when transitioning +} + +class _FlutterLogoModal extends StatelessWidget { + const _FlutterLogoModal(); + + @override + Widget build(BuildContext context) { + return const Center( + child: SizedBox( + width: 250, + height: 250, + child: Material( + child: Center( + child: FlutterLogo(size: 250), + ), + ), + ), + ); + } +} + +class _TestModalConfiguration extends ModalConfiguration { + /// Creates the Material fade transition configuration. + _TestModalConfiguration({ + bool barrierDismissible = true, + String barrierLabel, + }) : assert(barrierDismissible != null), + super( + barrierDismissible: barrierDismissible, + barrierLabel: barrierLabel, + ); + + @override + Color get barrierColor => Colors.green; + + @override + Duration get transitionDuration => const Duration(milliseconds: 300); + + @override + Duration get reverseTransitionDuration => const Duration(milliseconds: 200); + + @override + Widget transitionBuilder( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition( + opacity: animation, + child: child, + ); + } +} \ No newline at end of file From 33a2f35242e38f585fb8a5ad56d3fe2454cab270 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Mon, 13 Jan 2020 17:33:58 -0800 Subject: [PATCH 17/29] Add fade portion of forwards transition test --- .../animations/test/fade_transition_test.dart | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index 239e29082d3f..1e0a0d94b46e 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -41,6 +41,49 @@ void main() { ); // runs forward + testWidgets( + 'FadeTransitionConfiguration runs forward', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: RaisedButton( + onPressed: () { + showModal( + context: context, + configuration: FadeTransitionConfiguration(), + builder: (BuildContext context) { + return _FlutterLogoModal(key: key); + }, + ); + }, + child: Icon(Icons.add), + ), + ); + }), + ), + ), + ); + await tester.tap(find.byType(RaisedButton)); + await tester.pump(); + // Opacity duration: First 30ms linear transition at the + // start + double topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 0.0); + + await tester.pump(const Duration(milliseconds: 23)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, closeTo(0.5, 0.05)); + + await tester.pump(const Duration(milliseconds: 45)); + expect(find.byType(_FlutterLogoModal), findsOneWidget); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 1.0); + }, + ); // runs backwards @@ -49,8 +92,21 @@ void main() { // state is not lost when transitioning } +double _getOpacity(GlobalKey key, WidgetTester tester) { + final Finder finder = find.ancestor( + of: find.byKey(key), + matching: find.byType(FadeTransition), + ); + return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { + final FadeTransition transition = widget as FadeTransition; + return a * transition.opacity.value; + }); +} + class _FlutterLogoModal extends StatelessWidget { - const _FlutterLogoModal(); + const _FlutterLogoModal({ + Key key, + }) : super(key: key); @override Widget build(BuildContext context) { From f9b17fefcdd70b49e3f2f426734836beea091557 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Tue, 14 Jan 2020 09:57:27 -0800 Subject: [PATCH 18/29] Implement forwards and reverse test for fade transition --- .../animations/test/fade_transition_test.dart | 110 +++++++++++++++++- 1 file changed, 105 insertions(+), 5 deletions(-) diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index 1e0a0d94b46e..be6b48d84e51 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -69,23 +69,112 @@ void main() { ); await tester.tap(find.byType(RaisedButton)); await tester.pump(); - // Opacity duration: First 30ms linear transition at the - // start + // Opacity duration: First 30% of 150ms, linear transition double topFadeTransitionOpacity = _getOpacity(key, tester); + double topScale = _getScale(key, tester); expect(topFadeTransitionOpacity, 0.0); + expect(topScale, 0.80); + // 3/10 * 150ms = 45ms (total opacity animation duration) + // 1/2 * 45ms = ~23ms elapsed for halfway point of opacity + // animation await tester.pump(const Duration(milliseconds: 23)); topFadeTransitionOpacity = _getOpacity(key, tester); expect(topFadeTransitionOpacity, closeTo(0.5, 0.05)); + topScale = _getScale(key, tester); + expect(topScale, greaterThan(0.80)); + expect(topScale, lessThan(1.0)); - await tester.pump(const Duration(milliseconds: 45)); - expect(find.byType(_FlutterLogoModal), findsOneWidget); + // End of opacity animation + await tester.pump(const Duration(milliseconds: 22)); topFadeTransitionOpacity = _getOpacity(key, tester); expect(topFadeTransitionOpacity, 1.0); + topScale = _getScale(key, tester); + expect(topScale, greaterThan(0.80)); + expect(topScale, lessThan(1.0)); + + // 100ms into the animation + await tester.pump(const Duration(milliseconds: 55)); + topScale = _getScale(key, tester); + expect(topScale, greaterThan(0.80)); + expect(topScale, lessThan(1.0)); + + // Get to the end of the animation + await tester.pump(const Duration(milliseconds: 50)); + topScale = _getScale(key, tester); + expect(topScale, 1.0); + + await tester.pump(); + expect(find.byType(_FlutterLogoModal), findsOneWidget); }, ); // runs backwards + testWidgets( + 'FadeTransitionConfiguration runs forward', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: RaisedButton( + onPressed: () { + showModal( + context: context, + configuration: FadeTransitionConfiguration(), + builder: (BuildContext context) { + return _FlutterLogoModal(key: key); + }, + ); + }, + child: Icon(Icons.add), + ), + ); + }), + ), + ), + ); + // Show the incoming modal and let it animate in fully. + await tester.tap(find.byType(RaisedButton)); + await tester.pumpAndSettle(); + + // Tap on modal barrier to start reverse animation. + await tester.tapAt(Offset.zero); + await tester.pump(); + + // Opacity duration: Linear transition throughout 75ms + // No scale animations on exit transition. + double topFadeTransitionOpacity = _getOpacity(key, tester); + double topScale = _getScale(key, tester); + expect(topFadeTransitionOpacity, 1.0); + expect(topScale, 1.0); + + await tester.pump(const Duration(milliseconds: 25)); + topFadeTransitionOpacity = _getOpacity(key, tester); + topScale = _getScale(key, tester); + expect(topFadeTransitionOpacity, closeTo(0.66, 0.05)); + expect(topScale, 1.0); + + await tester.pump(const Duration(milliseconds: 25)); + topFadeTransitionOpacity = _getOpacity(key, tester); + topScale = _getScale(key, tester); + expect(topFadeTransitionOpacity, closeTo(0.33, 0.05)); + expect(topScale, 1.0); + + // End of opacity animation + await tester.pump(const Duration(milliseconds: 25)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 0.0); + topScale = _getScale(key, tester); + expect(topScale, 1.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.byType(_FlutterLogoModal), findsNothing); + }, + ); + // does not get interrupted when run in reverse @@ -98,11 +187,22 @@ double _getOpacity(GlobalKey key, WidgetTester tester) { matching: find.byType(FadeTransition), ); return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { - final FadeTransition transition = widget as FadeTransition; + final FadeTransition transition = widget; return a * transition.opacity.value; }); } +double _getScale(GlobalKey key, WidgetTester tester) { + final Finder finder = find.ancestor( + of: find.byKey(key), + matching: find.byType(ScaleTransition), + ); + return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { + final ScaleTransition transition = widget; + return a * transition.scale.value; + }); +} + class _FlutterLogoModal extends StatelessWidget { const _FlutterLogoModal({ Key key, From 9733e0feec11eb6289af2ccc6493e51bf880d7b4 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Tue, 14 Jan 2020 11:27:05 -0800 Subject: [PATCH 19/29] Implement animation interrupt test - Fix bug found from writing interrupt test --- .../animations/lib/src/fade_transition.dart | 139 +++++++++++++++++- .../animations/test/fade_transition_test.dart | 100 ++++++++++++- 2 files changed, 231 insertions(+), 8 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index e202d55b0627..7b5dff963785 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -85,26 +85,155 @@ class FadeTransitionConfiguration extends ModalConfiguration { Animation secondaryAnimation, Widget child, ) { - return AnimatedBuilder( + return _MaterialFadeTransition( animation: animation, + secondaryAnimation: secondaryAnimation, + child: child, + ); + } +} + +class _MaterialFadeTransition extends StatefulWidget { + const _MaterialFadeTransition({ + Key key, + @required this.animation, + @required this.secondaryAnimation, + this.child, + }) : assert(animation != null), + assert(secondaryAnimation != null), + super(key: key); + + /// The animation that drives the [child]'s entrance and exit. + /// + /// See also: + /// + /// * [TransitionRoute.animate], which is the value given to this property + /// when it is used as a page transition. + final Animation animation; + + /// The animation that transitions [child] when new content is pushed on top + /// of it. + /// + /// See also: + /// + /// * [TransitionRoute.secondaryAnimation], which is the value given to this + /// property when the it is used as a page transition. + final Animation secondaryAnimation; + + /// The widget below this widget in the tree. + /// + /// This widget will transition in and out as driven by [animation] and + /// [secondaryAnimation]. + final Widget child; + + @override + _MaterialFadeTransitionState createState() => _MaterialFadeTransitionState(); +} + +class _MaterialFadeTransitionState extends State<_MaterialFadeTransition> { + AnimationStatus _effectiveAnimationStatus; + + @override + void initState() { + super.initState(); + _effectiveAnimationStatus = widget.animation.status; + widget.animation.addStatusListener(_animationListener); + } + + void _animationListener(AnimationStatus animationStatus) { + _effectiveAnimationStatus = _calculateEffectiveAnimationStatus( + lastEffective: _effectiveAnimationStatus, + current: animationStatus, + ); + } + + // When a transition is interrupted midway we just want to play the ongoing + // animation in reverse. Switching to the actual reverse transition would + // yield a disjoint experience since the forward and reverse transitions are + // very different. + AnimationStatus _calculateEffectiveAnimationStatus({ + @required AnimationStatus lastEffective, + @required AnimationStatus current, + }) { + assert(current != null); + assert(lastEffective != null); + switch (current) { + case AnimationStatus.dismissed: + case AnimationStatus.completed: + return current; + case AnimationStatus.forward: + switch (lastEffective) { + case AnimationStatus.dismissed: + case AnimationStatus.completed: + case AnimationStatus.forward: + return current; + case AnimationStatus.reverse: + return lastEffective; + } + break; + case AnimationStatus.reverse: + switch (lastEffective) { + case AnimationStatus.dismissed: + case AnimationStatus.completed: + case AnimationStatus.reverse: + return current; + case AnimationStatus.forward: + return lastEffective; + } + break; + } + return null; // unreachable + } + + void _updateAnimationListener( + Animation oldAnimation, + Animation animation, + ) { + if (oldAnimation != animation) { + oldAnimation.removeStatusListener(_animationListener); + animation.addStatusListener(_animationListener); + _animationListener(animation.status); + } + } + + @override + void didUpdateWidget(_MaterialFadeTransition oldWidget) { + super.didUpdateWidget(oldWidget); + _updateAnimationListener( + oldWidget.animation, + widget.animation, + ); + } + + @override + void dispose() { + widget.animation.removeStatusListener(_animationListener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: widget.animation, builder: (BuildContext context, Widget child) { - switch (animation.status) { + assert(_effectiveAnimationStatus != null); + switch (_effectiveAnimationStatus) { case AnimationStatus.forward: return _EnterTransition( - animation: animation, + animation: widget.animation, child: child, ); case AnimationStatus.dismissed: case AnimationStatus.reverse: case AnimationStatus.completed: return FadeTransition( - opacity: animation, + opacity: widget.animation, child: child, ); } return null; // unreachable }, - child: child, + child: widget.child, ); } } diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index be6b48d84e51..bcc1449da411 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -40,7 +40,6 @@ void main() { }, ); - // runs forward testWidgets( 'FadeTransitionConfiguration runs forward', (WidgetTester tester) async { @@ -109,7 +108,6 @@ void main() { }, ); - // runs backwards testWidgets( 'FadeTransitionConfiguration runs forward', (WidgetTester tester) async { @@ -175,8 +173,104 @@ void main() { }, ); - // does not get interrupted when run in reverse + testWidgets( + 'FadeTransitionConfiguration does not jump when interrupted', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: RaisedButton( + onPressed: () { + showModal( + context: context, + configuration: FadeTransitionConfiguration(), + builder: (BuildContext context) { + return _FlutterLogoModal(key: key); + }, + ); + }, + child: Icon(Icons.add), + ), + ); + }), + ), + ), + ); + + await tester.tap(find.byType(RaisedButton)); + await tester.pump(); + // Opacity duration: First 30% of 150ms, linear transition + double topFadeTransitionOpacity = _getOpacity(key, tester); + double topScale = _getScale(key, tester); + expect(topFadeTransitionOpacity, 0.0); + expect(topScale, 0.80); + + // 3/10 * 150ms = 45ms (total opacity animation duration) + // End of opacity animation + await tester.pump(const Duration(milliseconds: 45)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 1.0); + topScale = _getScale(key, tester); + expect(topScale, greaterThan(0.80)); + expect(topScale, lessThan(1.0)); + + // 100ms into the animation + await tester.pump(const Duration(milliseconds: 55)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 1.0); + topScale = _getScale(key, tester); + expect(topScale, greaterThan(0.80)); + expect(topScale, lessThan(1.0)); + + // Start the reverse transition by interrupting the forwards + // transition. + await tester.tapAt(Offset.zero); + await tester.pump(); + // Opacity and scale values should remain the same after + // the reverse animation starts. + expect(_getOpacity(key, tester), topFadeTransitionOpacity); + expect(_getScale(key, tester), topScale); + + // Should animate in reverse with 2/3 * 75ms = 50ms + // using the enter transition's animation pattern + // instead of the exit animation pattern. + + // Calculation for the time when the linear fade + // transition should start if running backwards: + // 3/10 * 75ms = 22.5ms + // To get the 22.5ms timestamp, run backwards for: + // 50ms - 22.5ms = ~27.5ms + await tester.pump(const Duration(milliseconds: 27)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 1.0); + topScale = _getScale(key, tester); + expect(topScale, greaterThan(0.80)); + expect(topScale, lessThan(1.0)); + + // Halfway through fade animation + await tester.pump(const Duration(milliseconds: 12)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, closeTo(0.5, 0.05)); + topScale = _getScale(key, tester); + expect(topScale, greaterThan(0.80)); + expect(topScale, lessThan(1.0)); + + // Complete the rest of the animation + await tester.pump(const Duration(milliseconds: 11)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 0.0); + topScale = _getScale(key, tester); + expect(topScale, 0.8); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.byType(_FlutterLogoModal), findsNothing); + }, + ); + // state is not lost when transitioning } From 5845b3f6022c0e8373763dd3e9f18c910ec4bdb3 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Tue, 14 Jan 2020 11:42:22 -0800 Subject: [PATCH 20/29] Implement state test for fade transition --- .../animations/test/fade_transition_test.dart | 119 +++++++++++++++++- 1 file changed, 116 insertions(+), 3 deletions(-) diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index bcc1449da411..18d717a5d1aa 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -173,7 +173,6 @@ void main() { }, ); - // does not get interrupted when run in reverse testWidgets( 'FadeTransitionConfiguration does not jump when interrupted', (WidgetTester tester) async { @@ -271,8 +270,114 @@ void main() { }, ); + testWidgets('State is not lost when transitioning', (WidgetTester tester) async { + final GlobalKey bottomKey = GlobalKey(); + final GlobalKey topKey = GlobalKey(); - // state is not lost when transitioning + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: Column( + children: [ + RaisedButton( + onPressed: () { + showModal( + context: context, + configuration: FadeTransitionConfiguration(), + builder: (BuildContext context) { + return _FlutterLogoModal( + key: topKey, + name: 'top route', + ); + }, + ); + }, + child: Icon(Icons.add), + ), + _FlutterLogoModal( + key: bottomKey, + name: 'bottom route', + ), + ], + ), + ); + }), + ), + ), + ); + + final _FlutterLogoModalState bottomState = tester.state( + find.byKey(bottomKey), + ); + expect(bottomState.widget.name, 'bottom route'); + + await tester.tap(find.byType(RaisedButton)); + await tester.pump(); + await tester.pump(); + + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + final _FlutterLogoModalState topState = tester.state( + find.byKey(topKey), + ); + expect(topState.widget.name, 'top route'); + + await tester.pump(const Duration(milliseconds: 75)); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey( + bottomKey, + skipOffstage: false, + )), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + + await tester.tapAt(Offset.zero); + await tester.pump(); + + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + + await tester.pump(const Duration(milliseconds: 38)); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect(find.byKey(topKey), findsNothing); + }); } double _getOpacity(GlobalKey key, WidgetTester tester) { @@ -297,11 +402,19 @@ double _getScale(GlobalKey key, WidgetTester tester) { }); } -class _FlutterLogoModal extends StatelessWidget { +class _FlutterLogoModal extends StatefulWidget { const _FlutterLogoModal({ Key key, + this.name, }) : super(key: key); + final String name; + + @override + _FlutterLogoModalState createState() => _FlutterLogoModalState(); +} + +class _FlutterLogoModalState extends State<_FlutterLogoModal> { @override Widget build(BuildContext context) { return const Center( From 8474d44d424216ba7c6b664fa9339b3e93944c53 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Tue, 14 Jan 2020 12:04:50 -0800 Subject: [PATCH 21/29] modal.dart tests, comments on fade_transition_test.dart --- .../animations/test/fade_transition_test.dart | 15 +- packages/animations/test/modal_test.dart | 267 +++++++++++++++++- 2 files changed, 272 insertions(+), 10 deletions(-) diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index 18d717a5d1aa..4c2bbed3a896 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -308,24 +308,30 @@ void main() { ), ); + // The bottom route's state should already exist. final _FlutterLogoModalState bottomState = tester.state( find.byKey(bottomKey), ); expect(bottomState.widget.name, 'bottom route'); + // Start the enter transition of the modal route. await tester.tap(find.byType(RaisedButton)); await tester.pump(); await tester.pump(); + // The bottom route's state should be retained at the start of the + // transition. expect( tester.state(find.byKey(bottomKey)), bottomState, ); + // The top route's state should be created. final _FlutterLogoModalState topState = tester.state( find.byKey(topKey), ); expect(topState.widget.name, 'top route'); + // Halfway point of forwards animation. await tester.pump(const Duration(milliseconds: 75)); expect( tester.state(find.byKey(bottomKey)), @@ -336,6 +342,8 @@ void main() { topState, ); + // End the transition and see if top and bottom routes' + // states persist. await tester.pumpAndSettle(); expect( tester.state(find.byKey( @@ -349,9 +357,10 @@ void main() { topState, ); + // Start the reverse animation. Both top and bottom + // routes' states should persist. await tester.tapAt(Offset.zero); await tester.pump(); - expect( tester.state(find.byKey(bottomKey)), bottomState, @@ -361,6 +370,7 @@ void main() { topState, ); + // Halfway point of the exit transition. await tester.pump(const Duration(milliseconds: 38)); expect( tester.state(find.byKey(bottomKey)), @@ -371,6 +381,9 @@ void main() { topState, ); + // End the exit transition. The bottom route's state should + // persist, whereas the top route's state should no longer + // be present. await tester.pumpAndSettle(); expect( tester.state(find.byKey(bottomKey)), diff --git a/packages/animations/test/modal_test.dart b/packages/animations/test/modal_test.dart index ce9cf2daa76e..91a2e720e9f0 100644 --- a/packages/animations/test/modal_test.dart +++ b/packages/animations/test/modal_test.dart @@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart'; void main() { testWidgets( - 'showModal builds a new route', + 'showModal builds a new route with specified barrier properties', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -35,22 +35,272 @@ void main() { ); await tester.tap(find.byType(RaisedButton)); await tester.pumpAndSettle(); + + // New route containing _FlutterLogoModal is present. + expect(find.byType(_FlutterLogoModal), findsOneWidget); + final ModalBarrier topModalBarrier = tester.widget( + find.byType(ModalBarrier).at(1), + ); + + // Verify new route's modal barrier properties are correct. + expect(topModalBarrier.color, Colors.green); + expect(topModalBarrier.barrierSemanticsDismissible, true); + expect(topModalBarrier.semanticsLabel, 'customLabel'); + }, + ); + + testWidgets( + 'showModal forwards animation', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: RaisedButton( + onPressed: () { + showModal( + context: context, + configuration: _TestModalConfiguration(), + builder: (BuildContext context) { + return _FlutterLogoModal(key: key); + }, + ); + }, + child: Icon(Icons.add), + ), + ); + }), + ), + ), + ); + + // Start forwards animation + await tester.tap(find.byType(RaisedButton)); + await tester.pump(); + + // Opacity duration: Linear transition throughout 300ms + double topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 0.0); + + // Halfway through forwards animation. + await tester.pump(const Duration(milliseconds: 150)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 0.5); + + // The end of the transition. + await tester.pump(const Duration(milliseconds: 150)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 1.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.byType(_FlutterLogoModal), findsOneWidget); + }, + ); + + testWidgets( + 'showModal reverse animation', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: RaisedButton( + onPressed: () { + showModal( + context: context, + configuration: _TestModalConfiguration(), + builder: (BuildContext context) { + return _FlutterLogoModal(key: key); + }, + ); + }, + child: Icon(Icons.add), + ), + ); + }), + ), + ), + ); + + // Start forwards animation + await tester.tap(find.byType(RaisedButton)); + await tester.pumpAndSettle(); expect(find.byType(_FlutterLogoModal), findsOneWidget); + + await tester.tapAt(Offset.zero); + await tester.pump(); + + // Opacity duration: Linear transition throughout 200ms + double topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 1.0); + + // Halfway through forwards animation. + await tester.pump(const Duration(milliseconds: 100)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 0.5); + + // The end of the transition. + await tester.pump(const Duration(milliseconds: 100)); + topFadeTransitionOpacity = _getOpacity(key, tester); + expect(topFadeTransitionOpacity, 0.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.byType(_FlutterLogoModal), findsNothing); }, ); - // runs forward + testWidgets('State is not lost when transitioning', (WidgetTester tester) async { + final GlobalKey bottomKey = GlobalKey(); + final GlobalKey topKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: Column( + children: [ + RaisedButton( + onPressed: () { + showModal( + context: context, + configuration: _TestModalConfiguration(), + builder: (BuildContext context) { + return _FlutterLogoModal( + key: topKey, + name: 'top route', + ); + }, + ); + }, + child: Icon(Icons.add), + ), + _FlutterLogoModal( + key: bottomKey, + name: 'bottom route', + ), + ], + ), + ); + }), + ), + ), + ); + + // The bottom route's state should already exist. + final _FlutterLogoModalState bottomState = tester.state( + find.byKey(bottomKey), + ); + expect(bottomState.widget.name, 'bottom route'); - // runs backwards + // Start the enter transition of the modal route. + await tester.tap(find.byType(RaisedButton)); + await tester.pump(); + await tester.pump(); - // does not get interrupted when run in reverse + // The bottom route's state should be retained at the start of the + // transition. + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + // The top route's state should be created. + final _FlutterLogoModalState topState = tester.state( + find.byKey(topKey), + ); + expect(topState.widget.name, 'top route'); + + // Halfway point of forwards animation. + await tester.pump(const Duration(milliseconds: 150)); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + + // End the transition and see if top and bottom routes' + // states persist. + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey( + bottomKey, + skipOffstage: false, + )), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + + // Start the reverse animation. Both top and bottom + // routes' states should persist. + await tester.tapAt(Offset.zero); + await tester.pump(); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); - // state is not lost when transitioning + // Halfway point of the exit transition. + await tester.pump(const Duration(milliseconds: 100)); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + + // End the exit transition. The bottom route's state should + // persist, whereas the top route's state should no longer + // be present. + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect(find.byKey(topKey), findsNothing); + }); +} + + +double _getOpacity(GlobalKey key, WidgetTester tester) { + final Finder finder = find.ancestor( + of: find.byKey(key), + matching: find.byType(FadeTransition), + ); + return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { + final FadeTransition transition = widget; + return a * transition.opacity.value; + }); } -class _FlutterLogoModal extends StatelessWidget { - const _FlutterLogoModal(); +class _FlutterLogoModal extends StatefulWidget { + const _FlutterLogoModal({ + Key key, + this.name, + }) : super(key: key); + + final String name; + + @override + _FlutterLogoModalState createState() => _FlutterLogoModalState(); +} +class _FlutterLogoModalState extends State<_FlutterLogoModal> { @override Widget build(BuildContext context) { return const Center( @@ -68,10 +318,9 @@ class _FlutterLogoModal extends StatelessWidget { } class _TestModalConfiguration extends ModalConfiguration { - /// Creates the Material fade transition configuration. _TestModalConfiguration({ bool barrierDismissible = true, - String barrierLabel, + String barrierLabel = 'customLabel', }) : assert(barrierDismissible != null), super( barrierDismissible: barrierDismissible, From 69a4191edc84d1ca4bf97e17b01fb5f896a71991 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Tue, 14 Jan 2020 12:07:13 -0800 Subject: [PATCH 22/29] Newline at end of modal_test.dart --- packages/animations/test/modal_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/animations/test/modal_test.dart b/packages/animations/test/modal_test.dart index 91a2e720e9f0..00b686aa3598 100644 --- a/packages/animations/test/modal_test.dart +++ b/packages/animations/test/modal_test.dart @@ -348,4 +348,4 @@ class _TestModalConfiguration extends ModalConfiguration { child: child, ); } -} \ No newline at end of file +} From 3b50849c35a97818a0744706ae48264b703af8f0 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Tue, 14 Jan 2020 12:08:52 -0800 Subject: [PATCH 23/29] Make formatter happy --- .../animations/test/fade_transition_test.dart | 233 ++++++++--------- packages/animations/test/modal_test.dart | 234 +++++++++--------- 2 files changed, 236 insertions(+), 231 deletions(-) diff --git a/packages/animations/test/fade_transition_test.dart b/packages/animations/test/fade_transition_test.dart index 4c2bbed3a896..f09afd1c9594 100644 --- a/packages/animations/test/fade_transition_test.dart +++ b/packages/animations/test/fade_transition_test.dart @@ -270,127 +270,130 @@ void main() { }, ); - testWidgets('State is not lost when transitioning', (WidgetTester tester) async { - final GlobalKey bottomKey = GlobalKey(); - final GlobalKey topKey = GlobalKey(); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Builder(builder: (BuildContext context) { - return Center( - child: Column( - children: [ - RaisedButton( - onPressed: () { - showModal( - context: context, - configuration: FadeTransitionConfiguration(), - builder: (BuildContext context) { - return _FlutterLogoModal( - key: topKey, - name: 'top route', - ); - }, - ); - }, - child: Icon(Icons.add), - ), - _FlutterLogoModal( - key: bottomKey, - name: 'bottom route', - ), - ], - ), - ); - }), + testWidgets( + 'State is not lost when transitioning', + (WidgetTester tester) async { + final GlobalKey bottomKey = GlobalKey(); + final GlobalKey topKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: Column( + children: [ + RaisedButton( + onPressed: () { + showModal( + context: context, + configuration: FadeTransitionConfiguration(), + builder: (BuildContext context) { + return _FlutterLogoModal( + key: topKey, + name: 'top route', + ); + }, + ); + }, + child: Icon(Icons.add), + ), + _FlutterLogoModal( + key: bottomKey, + name: 'bottom route', + ), + ], + ), + ); + }), + ), ), - ), - ); + ); - // The bottom route's state should already exist. - final _FlutterLogoModalState bottomState = tester.state( - find.byKey(bottomKey), - ); - expect(bottomState.widget.name, 'bottom route'); - - // Start the enter transition of the modal route. - await tester.tap(find.byType(RaisedButton)); - await tester.pump(); - await tester.pump(); - - // The bottom route's state should be retained at the start of the - // transition. - expect( - tester.state(find.byKey(bottomKey)), - bottomState, - ); - // The top route's state should be created. - final _FlutterLogoModalState topState = tester.state( - find.byKey(topKey), - ); - expect(topState.widget.name, 'top route'); + // The bottom route's state should already exist. + final _FlutterLogoModalState bottomState = tester.state( + find.byKey(bottomKey), + ); + expect(bottomState.widget.name, 'bottom route'); - // Halfway point of forwards animation. - await tester.pump(const Duration(milliseconds: 75)); - expect( - tester.state(find.byKey(bottomKey)), - bottomState, - ); - expect( - tester.state(find.byKey(topKey)), - topState, - ); + // Start the enter transition of the modal route. + await tester.tap(find.byType(RaisedButton)); + await tester.pump(); + await tester.pump(); - // End the transition and see if top and bottom routes' - // states persist. - await tester.pumpAndSettle(); - expect( - tester.state(find.byKey( - bottomKey, - skipOffstage: false, - )), - bottomState, - ); - expect( - tester.state(find.byKey(topKey)), - topState, - ); + // The bottom route's state should be retained at the start of the + // transition. + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + // The top route's state should be created. + final _FlutterLogoModalState topState = tester.state( + find.byKey(topKey), + ); + expect(topState.widget.name, 'top route'); - // Start the reverse animation. Both top and bottom - // routes' states should persist. - await tester.tapAt(Offset.zero); - await tester.pump(); - expect( - tester.state(find.byKey(bottomKey)), - bottomState, - ); - expect( - tester.state(find.byKey(topKey)), - topState, - ); + // Halfway point of forwards animation. + await tester.pump(const Duration(milliseconds: 75)); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); - // Halfway point of the exit transition. - await tester.pump(const Duration(milliseconds: 38)); - expect( - tester.state(find.byKey(bottomKey)), - bottomState, - ); - expect( - tester.state(find.byKey(topKey)), - topState, - ); + // End the transition and see if top and bottom routes' + // states persist. + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey( + bottomKey, + skipOffstage: false, + )), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); - // End the exit transition. The bottom route's state should - // persist, whereas the top route's state should no longer - // be present. - await tester.pumpAndSettle(); - expect( - tester.state(find.byKey(bottomKey)), - bottomState, - ); - expect(find.byKey(topKey), findsNothing); - }); + // Start the reverse animation. Both top and bottom + // routes' states should persist. + await tester.tapAt(Offset.zero); + await tester.pump(); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + + // Halfway point of the exit transition. + await tester.pump(const Duration(milliseconds: 38)); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + + // End the exit transition. The bottom route's state should + // persist, whereas the top route's state should no longer + // be present. + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect(find.byKey(topKey), findsNothing); + }, + ); } double _getOpacity(GlobalKey key, WidgetTester tester) { @@ -419,7 +422,7 @@ class _FlutterLogoModal extends StatefulWidget { const _FlutterLogoModal({ Key key, this.name, - }) : super(key: key); + }) : super(key: key); final String name; diff --git a/packages/animations/test/modal_test.dart b/packages/animations/test/modal_test.dart index 00b686aa3598..731b1c57edad 100644 --- a/packages/animations/test/modal_test.dart +++ b/packages/animations/test/modal_test.dart @@ -153,129 +153,131 @@ void main() { }, ); - testWidgets('State is not lost when transitioning', (WidgetTester tester) async { - final GlobalKey bottomKey = GlobalKey(); - final GlobalKey topKey = GlobalKey(); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Builder(builder: (BuildContext context) { - return Center( - child: Column( - children: [ - RaisedButton( - onPressed: () { - showModal( - context: context, - configuration: _TestModalConfiguration(), - builder: (BuildContext context) { - return _FlutterLogoModal( - key: topKey, - name: 'top route', - ); - }, - ); - }, - child: Icon(Icons.add), - ), - _FlutterLogoModal( - key: bottomKey, - name: 'bottom route', - ), - ], - ), - ); - }), + testWidgets( + 'State is not lost when transitioning', + (WidgetTester tester) async { + final GlobalKey bottomKey = GlobalKey(); + final GlobalKey topKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (BuildContext context) { + return Center( + child: Column( + children: [ + RaisedButton( + onPressed: () { + showModal( + context: context, + configuration: _TestModalConfiguration(), + builder: (BuildContext context) { + return _FlutterLogoModal( + key: topKey, + name: 'top route', + ); + }, + ); + }, + child: Icon(Icons.add), + ), + _FlutterLogoModal( + key: bottomKey, + name: 'bottom route', + ), + ], + ), + ); + }), + ), ), - ), - ); + ); - // The bottom route's state should already exist. - final _FlutterLogoModalState bottomState = tester.state( - find.byKey(bottomKey), - ); - expect(bottomState.widget.name, 'bottom route'); - - // Start the enter transition of the modal route. - await tester.tap(find.byType(RaisedButton)); - await tester.pump(); - await tester.pump(); - - // The bottom route's state should be retained at the start of the - // transition. - expect( - tester.state(find.byKey(bottomKey)), - bottomState, - ); - // The top route's state should be created. - final _FlutterLogoModalState topState = tester.state( - find.byKey(topKey), - ); - expect(topState.widget.name, 'top route'); + // The bottom route's state should already exist. + final _FlutterLogoModalState bottomState = tester.state( + find.byKey(bottomKey), + ); + expect(bottomState.widget.name, 'bottom route'); - // Halfway point of forwards animation. - await tester.pump(const Duration(milliseconds: 150)); - expect( - tester.state(find.byKey(bottomKey)), - bottomState, - ); - expect( - tester.state(find.byKey(topKey)), - topState, - ); + // Start the enter transition of the modal route. + await tester.tap(find.byType(RaisedButton)); + await tester.pump(); + await tester.pump(); - // End the transition and see if top and bottom routes' - // states persist. - await tester.pumpAndSettle(); - expect( - tester.state(find.byKey( - bottomKey, - skipOffstage: false, - )), - bottomState, - ); - expect( - tester.state(find.byKey(topKey)), - topState, - ); + // The bottom route's state should be retained at the start of the + // transition. + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + // The top route's state should be created. + final _FlutterLogoModalState topState = tester.state( + find.byKey(topKey), + ); + expect(topState.widget.name, 'top route'); - // Start the reverse animation. Both top and bottom - // routes' states should persist. - await tester.tapAt(Offset.zero); - await tester.pump(); - expect( - tester.state(find.byKey(bottomKey)), - bottomState, - ); - expect( - tester.state(find.byKey(topKey)), - topState, - ); + // Halfway point of forwards animation. + await tester.pump(const Duration(milliseconds: 150)); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); - // Halfway point of the exit transition. - await tester.pump(const Duration(milliseconds: 100)); - expect( - tester.state(find.byKey(bottomKey)), - bottomState, - ); - expect( - tester.state(find.byKey(topKey)), - topState, - ); + // End the transition and see if top and bottom routes' + // states persist. + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey( + bottomKey, + skipOffstage: false, + )), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); - // End the exit transition. The bottom route's state should - // persist, whereas the top route's state should no longer - // be present. - await tester.pumpAndSettle(); - expect( - tester.state(find.byKey(bottomKey)), - bottomState, - ); - expect(find.byKey(topKey), findsNothing); - }); -} + // Start the reverse animation. Both top and bottom + // routes' states should persist. + await tester.tapAt(Offset.zero); + await tester.pump(); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + // Halfway point of the exit transition. + await tester.pump(const Duration(milliseconds: 100)); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect( + tester.state(find.byKey(topKey)), + topState, + ); + + // End the exit transition. The bottom route's state should + // persist, whereas the top route's state should no longer + // be present. + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey(bottomKey)), + bottomState, + ); + expect(find.byKey(topKey), findsNothing); + }, + ); +} double _getOpacity(GlobalKey key, WidgetTester tester) { final Finder finder = find.ancestor( @@ -292,7 +294,7 @@ class _FlutterLogoModal extends StatefulWidget { const _FlutterLogoModal({ Key key, this.name, - }) : super(key: key); + }) : super(key: key); final String name; From 45dd4bfbd64000fa46036b9f991eab69fcb5cf9e Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Thu, 16 Jan 2020 10:30:26 -0800 Subject: [PATCH 24/29] Code review feedback --- .../animations/lib/src/fade_transition.dart | 2 + packages/animations/lib/src/modal.dart | 75 ++++++++++--------- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index 7b5dff963785..aab3fa79afe0 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -60,6 +60,8 @@ import 'utils/curves.dart'; /// ``` class FadeTransitionConfiguration extends ModalConfiguration { /// Creates the Material fade transition configuration. + /// + /// [barrierDismissible] cannot be null. FadeTransitionConfiguration({ bool barrierDismissible = true, String barrierLabel, diff --git a/packages/animations/lib/src/modal.dart b/packages/animations/lib/src/modal.dart index 438267e1f957..2a4e1159131c 100644 --- a/packages/animations/lib/src/modal.dart +++ b/packages/animations/lib/src/modal.dart @@ -75,46 +75,42 @@ Future showModal({ class _ModalRoute extends PopupRoute { /// Creates a [_ModalRoute] route with the Material fade transition. /// - /// [barrierDismissible] is true by default. + /// [barrierDismissible] configures whether or not tapping the modal's + /// scrim dismisses the modal. [barrierLabel] sets the semantic label for + /// a dismissible barrier. [barrierDismissible] cannot be null. If + /// [barrierDismissible] is true, the [barrierLabel] cannot be null. _ModalRoute({ - Color barrierColor, - bool barrierDismissible = true, - String barrierLabel, + this.barrierColor, + this.barrierDismissible = true, + this.barrierLabel, + this.transitionDuration, + this.reverseTransitionDuration, _ModalTransitionBuilder transitionBuilder, - Duration transitionDuration, - Duration reverseTransitionDuration, @required this.builder, }) : assert(barrierDismissible != null), - _barrierColor = barrierColor, - _barrierDismissible = barrierDismissible, - _barrierLabel = barrierLabel, - _transitionBuilder = transitionBuilder, - _transitionDuration = transitionDuration, - _reverseTransitionDuration = reverseTransitionDuration; + assert(!barrierDismissible || barrierLabel != null), + _transitionBuilder = transitionBuilder; @override - Color get barrierColor => _barrierColor; - final Color _barrierColor; + final Color barrierColor; @override - bool get barrierDismissible => _barrierDismissible; - final bool _barrierDismissible; + final bool barrierDismissible; @override - String get barrierLabel => _barrierLabel; - final String _barrierLabel; + final String barrierLabel; @override - Duration get transitionDuration => _transitionDuration; - final Duration _transitionDuration; + final Duration transitionDuration; @override - Duration get reverseTransitionDuration => _reverseTransitionDuration; - final Duration _reverseTransitionDuration; + final Duration reverseTransitionDuration; /// The primary contents of the modal. final WidgetBuilder builder; + final _ModalTransitionBuilder _transitionBuilder; + @override Widget buildPage( BuildContext context, @@ -150,8 +146,6 @@ class _ModalRoute extends PopupRoute { child, ); } - - final _ModalTransitionBuilder _transitionBuilder; } /// A configuration object containing the properties needed to implement a @@ -167,13 +161,26 @@ class _ModalRoute extends PopupRoute { abstract class ModalConfiguration { /// Creates a modal configuration object that provides the necessary /// properties to implement a modal route. + /// + /// [barrierDismissible] configures whether or not tapping the modal's + /// scrim dismisses the modal. [barrierLabel] sets the semantic label for + /// a dismissible barrier. [barrierDismissible] cannot be null. If + /// [barrierDismissible] is true, the [barrierLabel] cannot be null. + /// + /// [transitionDuration] and [reverseTransitionDuration] determine the + /// duration of the transitions when the modal enters and exits the + /// application. [transitionDuration] and [reverseTransitionDuration] + /// cannot be null. ModalConfiguration({ this.barrierColor, - this.barrierDismissible, + @required this.barrierDismissible, this.barrierLabel, - this.transitionDuration, - this.reverseTransitionDuration, - }); + @required this.transitionDuration, + @required this.reverseTransitionDuration, + }) : assert(barrierDismissible != null), + assert(transitionDuration != null), + assert(reverseTransitionDuration != null), + assert(!barrierDismissible || barrierLabel != null); /// The color to use for the modal barrier. If this is null, the barrier will /// be transparent. @@ -185,6 +192,12 @@ abstract class ModalConfiguration { /// The semantic label used for a dismissible barrier. final String barrierLabel; + /// The duration of the transition running forwards. + final Duration transitionDuration; + + /// The duration of the transition running in reverse. + final Duration reverseTransitionDuration; + /// A builder that defines how the route arrives on and leaves the screen. /// /// The [buildTransitions] method is typically used to define transitions @@ -199,10 +212,4 @@ abstract class ModalConfiguration { Animation secondaryAnimation, Widget child, ); - - /// The duration of the transition running forwards. - final Duration transitionDuration; - - /// The duration of the transition running in reverse. - final Duration reverseTransitionDuration; } From b812d348f472c4fd251c9347f6ce883a08d37de3 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Thu, 16 Jan 2020 10:36:49 -0800 Subject: [PATCH 25/29] Code cleanup --- packages/animations/lib/src/fade_transition.dart | 8 +++++--- packages/animations/lib/src/modal.dart | 11 ++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index aab3fa79afe0..d23c52bbce5a 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -61,12 +61,14 @@ import 'utils/curves.dart'; class FadeTransitionConfiguration extends ModalConfiguration { /// Creates the Material fade transition configuration. /// - /// [barrierDismissible] cannot be null. + /// [barrierDismissible] configures whether or not tapping the modal's + /// scrim dismisses the modal. [barrierLabel] sets the semantic label for + /// a dismissible barrier. [barrierDismissible] cannot be null. If + /// [barrierDismissible] is true, the [barrierLabel] cannot be null. FadeTransitionConfiguration({ bool barrierDismissible = true, String barrierLabel, - }) : assert(barrierDismissible != null), - super( + }) : super( barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, ); diff --git a/packages/animations/lib/src/modal.dart b/packages/animations/lib/src/modal.dart index 2a4e1159131c..f9d1c927f353 100644 --- a/packages/animations/lib/src/modal.dart +++ b/packages/animations/lib/src/modal.dart @@ -73,12 +73,15 @@ Future showModal({ // A modal route that overlays a widget on the current route. class _ModalRoute extends PopupRoute { - /// Creates a [_ModalRoute] route with the Material fade transition. + /// Creates a route with general modal route. /// /// [barrierDismissible] configures whether or not tapping the modal's /// scrim dismisses the modal. [barrierLabel] sets the semantic label for /// a dismissible barrier. [barrierDismissible] cannot be null. If /// [barrierDismissible] is true, the [barrierLabel] cannot be null. + /// + /// [transitionBuilder] takes in a function that creates a widget. This + /// widget is typically used to configure the modal's transition. _ModalRoute({ this.barrierColor, this.barrierDismissible = true, @@ -175,11 +178,9 @@ abstract class ModalConfiguration { this.barrierColor, @required this.barrierDismissible, this.barrierLabel, - @required this.transitionDuration, - @required this.reverseTransitionDuration, + this.transitionDuration, + this.reverseTransitionDuration, }) : assert(barrierDismissible != null), - assert(transitionDuration != null), - assert(reverseTransitionDuration != null), assert(!barrierDismissible || barrierLabel != null); /// The color to use for the modal barrier. If this is null, the barrier will From 64846d8d07a4392b25dfd0647c69da3f9cab2cd8 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Thu, 16 Jan 2020 10:45:47 -0800 Subject: [PATCH 26/29] Make formatter happy --- packages/animations/lib/src/fade_transition.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index d23c52bbce5a..18555c6b9ae9 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -68,7 +68,7 @@ class FadeTransitionConfiguration extends ModalConfiguration { FadeTransitionConfiguration({ bool barrierDismissible = true, String barrierLabel, - }) : super( + }) : super( barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, ); From 614687cd2543b1b4a0d4ef9a243292dbfbaa96c9 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Thu, 16 Jan 2020 11:00:22 -0800 Subject: [PATCH 27/29] abstract getters over constructor parameters --- packages/animations/lib/src/modal.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/animations/lib/src/modal.dart b/packages/animations/lib/src/modal.dart index f9d1c927f353..3f3751ea95d6 100644 --- a/packages/animations/lib/src/modal.dart +++ b/packages/animations/lib/src/modal.dart @@ -175,17 +175,14 @@ abstract class ModalConfiguration { /// application. [transitionDuration] and [reverseTransitionDuration] /// cannot be null. ModalConfiguration({ - this.barrierColor, @required this.barrierDismissible, this.barrierLabel, - this.transitionDuration, - this.reverseTransitionDuration, }) : assert(barrierDismissible != null), assert(!barrierDismissible || barrierLabel != null); /// The color to use for the modal barrier. If this is null, the barrier will /// be transparent. - final Color barrierColor; + Color get barrierColor; /// Whether you can dismiss this route by tapping the modal barrier. final bool barrierDismissible; @@ -194,10 +191,10 @@ abstract class ModalConfiguration { final String barrierLabel; /// The duration of the transition running forwards. - final Duration transitionDuration; + Duration get transitionDuration; /// The duration of the transition running in reverse. - final Duration reverseTransitionDuration; + Duration get reverseTransitionDuration; /// A builder that defines how the route arrives on and leaves the screen. /// From a9968c7dc689f8bdb001a727c00ea31112bc54c4 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Thu, 16 Jan 2020 11:10:50 -0800 Subject: [PATCH 28/29] Abstract getter -> constructor params --- .../animations/lib/src/fade_transition.dart | 17 +++++++---------- packages/animations/lib/src/modal.dart | 16 +++++++++++----- packages/animations/test/modal_test.dart | 15 ++++++--------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/animations/lib/src/fade_transition.dart b/packages/animations/lib/src/fade_transition.dart index 18555c6b9ae9..077f6e6b3a43 100644 --- a/packages/animations/lib/src/fade_transition.dart +++ b/packages/animations/lib/src/fade_transition.dart @@ -66,22 +66,19 @@ class FadeTransitionConfiguration extends ModalConfiguration { /// a dismissible barrier. [barrierDismissible] cannot be null. If /// [barrierDismissible] is true, the [barrierLabel] cannot be null. FadeTransitionConfiguration({ + Color barrierColor = Colors.black54, bool barrierDismissible = true, - String barrierLabel, + Duration transitionDuration = const Duration(milliseconds: 150), + Duration reverseTransitionDuration = const Duration(milliseconds: 75), + String barrierLabel = 'Dismiss', }) : super( + barrierColor: barrierColor, barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, + transitionDuration: transitionDuration, + reverseTransitionDuration: reverseTransitionDuration, ); - @override - Color get barrierColor => Colors.black54; - - @override - Duration get transitionDuration => const Duration(milliseconds: 150); - - @override - Duration get reverseTransitionDuration => const Duration(milliseconds: 75); - @override Widget transitionBuilder( BuildContext context, diff --git a/packages/animations/lib/src/modal.dart b/packages/animations/lib/src/modal.dart index 3f3751ea95d6..c03662199026 100644 --- a/packages/animations/lib/src/modal.dart +++ b/packages/animations/lib/src/modal.dart @@ -175,14 +175,20 @@ abstract class ModalConfiguration { /// application. [transitionDuration] and [reverseTransitionDuration] /// cannot be null. ModalConfiguration({ + @required this.barrierColor, @required this.barrierDismissible, this.barrierLabel, - }) : assert(barrierDismissible != null), - assert(!barrierDismissible || barrierLabel != null); + @required this.transitionDuration, + @required this.reverseTransitionDuration, + }) : assert(barrierColor != null), + assert(barrierDismissible != null), + assert(!barrierDismissible || barrierLabel != null), + assert(transitionDuration != null), + assert(reverseTransitionDuration != null); /// The color to use for the modal barrier. If this is null, the barrier will /// be transparent. - Color get barrierColor; + final Color barrierColor; /// Whether you can dismiss this route by tapping the modal barrier. final bool barrierDismissible; @@ -191,10 +197,10 @@ abstract class ModalConfiguration { final String barrierLabel; /// The duration of the transition running forwards. - Duration get transitionDuration; + final Duration transitionDuration; /// The duration of the transition running in reverse. - Duration get reverseTransitionDuration; + final Duration reverseTransitionDuration; /// A builder that defines how the route arrives on and leaves the screen. /// diff --git a/packages/animations/test/modal_test.dart b/packages/animations/test/modal_test.dart index 731b1c57edad..bb21563a3489 100644 --- a/packages/animations/test/modal_test.dart +++ b/packages/animations/test/modal_test.dart @@ -321,23 +321,20 @@ class _FlutterLogoModalState extends State<_FlutterLogoModal> { class _TestModalConfiguration extends ModalConfiguration { _TestModalConfiguration({ + Color barrierColor = Colors.green, bool barrierDismissible = true, String barrierLabel = 'customLabel', + Duration transitionDuration = const Duration(milliseconds: 300), + Duration reverseTransitionDuration = const Duration(milliseconds: 150), }) : assert(barrierDismissible != null), super( + barrierColor: barrierColor, barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, + transitionDuration: transitionDuration, + reverseTransitionDuration: reverseTransitionDuration, ); - @override - Color get barrierColor => Colors.green; - - @override - Duration get transitionDuration => const Duration(milliseconds: 300); - - @override - Duration get reverseTransitionDuration => const Duration(milliseconds: 200); - @override Widget transitionBuilder( BuildContext context, From ae78df893558507ee1cf2a19060a63834c977b45 Mon Sep 17 00:00:00 2001 From: Shi Hao Hong Date: Thu, 16 Jan 2020 11:18:37 -0800 Subject: [PATCH 29/29] Fix incorrect reverse transition duration in modal_test.dart --- packages/animations/test/modal_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/animations/test/modal_test.dart b/packages/animations/test/modal_test.dart index bb21563a3489..135768ace458 100644 --- a/packages/animations/test/modal_test.dart +++ b/packages/animations/test/modal_test.dart @@ -325,7 +325,7 @@ class _TestModalConfiguration extends ModalConfiguration { bool barrierDismissible = true, String barrierLabel = 'customLabel', Duration transitionDuration = const Duration(milliseconds: 300), - Duration reverseTransitionDuration = const Duration(milliseconds: 150), + Duration reverseTransitionDuration = const Duration(milliseconds: 200), }) : assert(barrierDismissible != null), super( barrierColor: barrierColor,