Commit db08afd0 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by Flutter GitHub Bot

Allow for customizable ModalRoute barrierTween (#48345)

parent 1613a62e
......@@ -998,7 +998,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
///
/// 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 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// 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 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [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 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
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 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
);
}
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,
);
......
......@@ -1034,6 +1034,133 @@ void main() {
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 @@ class DialogObserver extends NavigatorObserver {
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,
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment