Allow for customizable ModalRoute barrierTween (#48345)
diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 2ad8276..46527ae 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart
@@ -998,7 +998,7 @@ /// /// If [barrierDismissible] is false, then tapping the barrier has no effect. /// - /// If this getter would ever start returning a different color, + /// If this getter would ever start returning a different value, /// [changedInternalState] should be invoked so that the change can take /// effect. /// @@ -1062,7 +1062,7 @@ /// For example, when a dialog is on the screen, the page below the dialog is /// usually darkened by the modal barrier. /// - /// If this getter would ever start returning a different color, + /// If this getter would ever start returning a different label, /// [changedInternalState] should be invoked so that the change can take /// effect. /// @@ -1073,6 +1073,32 @@ /// * [ModalBarrier], the widget that implements this feature. String get barrierLabel; + /// The curve that is used for animating the modal barrier in and out. + /// + /// The modal barrier is the scrim that is rendered behind each route, which + /// generally prevents the user from interacting with the route below the + /// current route, and normally partially obscures such routes. + /// + /// For example, when a dialog is on the screen, the page below the dialog is + /// usually darkened by the modal barrier. + /// + /// While the route is animating into position, the color is animated from + /// transparent to the specified [barrierColor]. + /// + /// If this getter would ever start returning a different curve, + /// [changedInternalState] should be invoked so that the change can take + /// effect. + /// + /// It defaults to [Curves.ease]. + /// + /// See also: + /// + /// * [barrierColor], which determines the color that the modal transitions + /// to. + /// * [Curves] for a collection of common curves. + /// * [AnimatedModalBarrier], the widget that implements this feature. + Curve get barrierCurve => Curves.ease; + /// Whether the route should remain in memory when it is inactive. /// /// If this is true, then the route is maintained, so that any futures it is @@ -1282,30 +1308,28 @@ final GlobalKey _subtreeKey = GlobalKey(); final PageStorageBucket _storageBucket = PageStorageBucket(); - static final Animatable<double> _easeCurveTween = CurveTween(curve: Curves.ease); - // one of the builders OverlayEntry _modalBarrier; Widget _buildModalBarrier(BuildContext context) { Widget barrier; - if (barrierColor != null && !offstage) { // changedInternalState is called if these update + if (barrierColor != null && !offstage) { // changedInternalState is called if barrierColor or offstage updates assert(barrierColor != _kTransparent); final Animation<Color> color = animation.drive( ColorTween( begin: _kTransparent, - end: barrierColor, // changedInternalState is called if this updates - ).chain(_easeCurveTween), + end: barrierColor, // changedInternalState is called if barrierColor updates + ).chain(CurveTween(curve: barrierCurve)), // changedInternalState is called if barrierCurve updates ); barrier = AnimatedModalBarrier( color: color, - dismissible: barrierDismissible, // changedInternalState is called if this updates - semanticsLabel: barrierLabel, // changedInternalState is called if this updates + dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates + semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates barrierSemanticsDismissible: semanticsDismissible, ); } else { barrier = ModalBarrier( - dismissible: barrierDismissible, // changedInternalState is called if this updates - semanticsLabel: barrierLabel, // changedInternalState is called if this updates + dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates + semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates barrierSemanticsDismissible: semanticsDismissible, ); } @@ -1316,7 +1340,7 @@ ); } return IgnorePointer( - ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates + ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when animation.status updates animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture child: barrier, );
diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart index d309473..9869a7f 100644 --- a/packages/flutter/test/widgets/routes_test.dart +++ b/packages/flutter/test/widgets/routes_test.dart
@@ -1034,6 +1034,133 @@ expect(find.byKey(containerKey), findsNothing); }); }); + + group('ModalRoute', () { + testWidgets('default barrierCurve', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Material( + child: Builder( + builder: (BuildContext context) { + return Center( + child: RaisedButton( + child: const Text('X'), + onPressed: () { + Navigator.of(context).push<void>( + _TestDialogRouteWithCustomBarrierCurve<void>( + child: const Text('Hello World'), + ) + ); + }, + ), + ); + } + ), + ), + )); + + final CurveTween _defaultBarrierTween = CurveTween(curve: Curves.ease); + int _getExpectedBarrierTweenAlphaValue(double t) { + return Color.getAlphaFromOpacity(_defaultBarrierTween.transform(t)); + } + + await tester.tap(find.text('X')); + await tester.pump(); + final Finder animatedModalBarrier = find.byType(AnimatedModalBarrier); + expect(animatedModalBarrier, findsOneWidget); + + Animation<Color> modalBarrierAnimation; + modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; + expect(modalBarrierAnimation.value, Colors.transparent); + + await tester.pump(const Duration(milliseconds: 25)); + modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; + expect( + modalBarrierAnimation.value.alpha, + closeTo(_getExpectedBarrierTweenAlphaValue(0.25), 1.0), + ); + + await tester.pump(const Duration(milliseconds: 25)); + modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; + expect( + modalBarrierAnimation.value.alpha, + closeTo(_getExpectedBarrierTweenAlphaValue(0.50), 1.0), + ); + + await tester.pump(const Duration(milliseconds: 25)); + modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; + expect( + modalBarrierAnimation.value.alpha, + closeTo(_getExpectedBarrierTweenAlphaValue(0.75), 1.0), + ); + + await tester.pumpAndSettle(); + modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; + expect(modalBarrierAnimation.value, Colors.black); + }); + + testWidgets('custom barrierCurve', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Material( + child: Builder( + builder: (BuildContext context) { + return Center( + child: RaisedButton( + child: const Text('X'), + onPressed: () { + Navigator.of(context).push<void>( + _TestDialogRouteWithCustomBarrierCurve<void>( + child: const Text('Hello World'), + barrierCurve: Curves.linear, + ), + ); + }, + ), + ); + }, + ), + ), + )); + + final CurveTween _customBarrierTween = CurveTween(curve: Curves.linear); + int _getExpectedBarrierTweenAlphaValue(double t) { + return Color.getAlphaFromOpacity(_customBarrierTween.transform(t)); + } + + await tester.tap(find.text('X')); + await tester.pump(); + final Finder animatedModalBarrier = find.byType(AnimatedModalBarrier); + expect(animatedModalBarrier, findsOneWidget); + + Animation<Color> modalBarrierAnimation; + modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; + expect(modalBarrierAnimation.value, Colors.transparent); + + await tester.pump(const Duration(milliseconds: 25)); + modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; + expect( + modalBarrierAnimation.value.alpha, + closeTo(_getExpectedBarrierTweenAlphaValue(0.25), 1.0), + ); + + await tester.pump(const Duration(milliseconds: 25)); + modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; + expect( + modalBarrierAnimation.value.alpha, + closeTo(_getExpectedBarrierTweenAlphaValue(0.50), 1.0), + ); + + await tester.pump(const Duration(milliseconds: 25)); + modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; + expect( + modalBarrierAnimation.value.alpha, + closeTo(_getExpectedBarrierTweenAlphaValue(0.75), 1.0), + ); + + await tester.pumpAndSettle(); + modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; + expect(modalBarrierAnimation.value, Colors.black); + }); + }); } double _getOpacity(GlobalKey key, WidgetTester tester) { @@ -1089,3 +1216,43 @@ super.didPush(route, previousRoute); } } + +class _TestDialogRouteWithCustomBarrierCurve<T> extends PopupRoute<T> { + _TestDialogRouteWithCustomBarrierCurve({ + @required Widget child, + Curve barrierCurve, + }) : _barrierCurve = barrierCurve, + _child = child; + + final Widget _child; + + @override + bool get barrierDismissible => true; + + @override + String get barrierLabel => null; + + @override + Color get barrierColor => Colors.black; // easier value to test against + + @override + Curve get barrierCurve { + if (_barrierCurve == null) { + return super.barrierCurve; + } + return _barrierCurve; + } + final Curve _barrierCurve; + + @override + Duration get transitionDuration => const Duration(milliseconds: 100); // easier value to test against + + @override + Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { + return Semantics( + child: _child, + scopesRoute: true, + explicitChildNodes: true, + ); + } +}