Unverified Commit 345d939e authored by Natalie Sampsell's avatar Natalie Sampsell Committed by GitHub

Add showCupertinoDialog and showGeneralDialog (#20152)

parent e770685a
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -9,6 +11,9 @@ import 'package:flutter/widgets.dart'; ...@@ -9,6 +11,9 @@ import 'package:flutter/widgets.dart';
const double _kBackGestureWidth = 20.0; const double _kBackGestureWidth = 20.0;
const double _kMinFlingVelocity = 1.0; // Screen widths per second. const double _kMinFlingVelocity = 1.0; // Screen widths per second.
// Barrier color for a Cupertino modal barrier.
const Color _kModalBarrierColor = Color(0x6604040F);
// Offset from offscreen to the right to fully on screen. // Offset from offscreen to the right to fully on screen.
final Tween<Offset> _kRightMiddleTween = new Tween<Offset>( final Tween<Offset> _kRightMiddleTween = new Tween<Offset>(
begin: const Offset(1.0, 0.0), begin: const Offset(1.0, 0.0),
...@@ -709,3 +714,77 @@ class _CupertinoEdgeShadowPainter extends BoxPainter { ...@@ -709,3 +714,77 @@ class _CupertinoEdgeShadowPainter extends BoxPainter {
canvas.drawRect(rect, paint); canvas.drawRect(rect, paint);
} }
} }
Widget _buildCupertinoDialogTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
final CurvedAnimation fadeAnimation = new CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
);
if (animation.status == AnimationStatus.reverse) {
return new FadeTransition(
opacity: fadeAnimation,
child: child,
);
}
return new FadeTransition(
opacity: fadeAnimation,
child: ScaleTransition(
child: child,
scale: new Tween<double>(
begin: 1.2,
end: 1.0,
).animate(
new CurvedAnimation(
parent: animation,
curve: Curves.fastOutSlowIn,
),
),
),
);
}
/// Displays an iOS-style dialog above the current contents of the app, with
/// iOS-style entrance and exit animations, modal barrier color, and modal
/// barrier behavior (the dialog is not dismissible with a tap on the barrier).
///
/// This function takes a `builder` which typically builds a [CupertinoDialog]
/// or [CupertinoAlertDialog] widget. Content below the dialog is dimmed with a
/// [ModalBarrier]. The widget returned by the `builder` does not share a
/// context with the location that `showCupertinoDialog` is originally called
/// from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the dialog
/// needs to update dynamically.
///
/// 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.
///
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
/// 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)`.
///
/// See also:
/// * [CupertinoDialog], an iOS-style dialog.
/// * [CupertinoAlertDialog], an iOS-style alert dialog.
/// * [showDialog], which displays a Material-style dialog.
/// * [showGeneralDialog], which allows for customization of the dialog popup.
/// * <https://developer.apple.com/ios/human-interface-guidelines/views/alerts/>
Future<T> showCupertinoDialog<T>({
@required BuildContext context,
@required WidgetBuilder builder,
}) {
assert(builder != null);
return showGeneralDialog(
context: context,
barrierDismissible: false,
barrierColor: _kModalBarrierColor,
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return builder(context);
},
transitionBuilder: _buildCupertinoDialogTransitions,
);
}
\ No newline at end of file
...@@ -543,70 +543,25 @@ class SimpleDialog extends StatelessWidget { ...@@ -543,70 +543,25 @@ class SimpleDialog extends StatelessWidget {
} }
} }
class _DialogRoute<T> extends PopupRoute<T> { Widget _buildMaterialDialogTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
_DialogRoute({ return new FadeTransition(
@required this.theme, opacity: new CurvedAnimation(
bool barrierDismissible = true, parent: animation,
this.barrierLabel, curve: Curves.easeOut,
@required this.child, ),
RouteSettings settings, child: child,
}) : assert(barrierDismissible != null), );
_barrierDismissible = barrierDismissible,
super(settings: settings);
final Widget child;
final ThemeData theme;
@override
Duration get transitionDuration => const Duration(milliseconds: 150);
@override
bool get barrierDismissible => _barrierDismissible;
final bool _barrierDismissible;
@override
Color get barrierColor => Colors.black54;
@override
final String barrierLabel;
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return new SafeArea(
child: new Builder(
builder: (BuildContext context) {
final Widget annotatedChild = new Semantics(
child: child,
scopesRoute: true,
explicitChildNodes: true,
);
return theme != null
? new Theme(data: theme, child: annotatedChild)
: annotatedChild;
}
),
);
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return new FadeTransition(
opacity: new CurvedAnimation(
parent: animation,
curve: Curves.easeOut
),
child: child
);
}
} }
/// Displays a dialog above the current contents of the app. /// Displays a Material dialog above the current contents of the app, with
/// Material entrance and exit animations, modal barrier color, and modal
/// barrier behavior (dialog is dismissible with a tap on the barrier).
/// ///
/// This function takes a `builder` which typically builds a [Dialog] widget. /// This function takes a `builder` which typically builds a [Dialog] widget.
/// Content below the dialog is dimmed with a [ModalBarrier]. This widget does /// Content below the dialog is dimmed with a [ModalBarrier]. The widget
/// not share a context with the location that `showDialog` is originally /// returned by the `builder` does not share a context with the location that
/// called from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the /// `showDialog` is originally called from. Use a [StatefulBuilder] or a
/// dialog needs to update dynamically. /// custom [StatefulWidget] if the dialog needs to update dynamically.
/// ///
/// The `context` argument is used to look up the [Navigator] and [Theme] for /// The `context` argument is used to look up the [Navigator] and [Theme] for
/// the dialog. It is only used when the method is called. Its corresponding /// the dialog. It is only used when the method is called. Its corresponding
...@@ -620,13 +575,15 @@ class _DialogRoute<T> extends PopupRoute<T> { ...@@ -620,13 +575,15 @@ class _DialogRoute<T> extends PopupRoute<T> {
/// The dialog route created by this method is pushed to the root navigator. /// 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 /// If the application has multiple [Navigator] objects, it may be necessary to
/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the /// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
/// dialog rather just 'Navigator.pop(context, result)`. /// dialog rather than just `Navigator.pop(context, result)`.
/// ///
/// See also: /// See also:
/// * [AlertDialog], for dialogs that have a row of buttons below a body. /// * [AlertDialog], for dialogs that have a row of buttons below a body.
/// * [SimpleDialog], which handles the scrolling of the contents and does /// * [SimpleDialog], which handles the scrolling of the contents and does
/// not show buttons below its body. /// not show buttons below its body.
/// * [Dialog], on which [SimpleDialog] and [AlertDialog] are based. /// * [Dialog], on which [SimpleDialog] and [AlertDialog] are based.
/// * [showCupertinoDialog], which displays an iOS-style dialog.
/// * [showGeneralDialog], which allows for customization of the dialog popup.
/// * <https://material.google.com/components/dialogs.html> /// * <https://material.google.com/components/dialogs.html>
Future<T> showDialog<T>({ Future<T> showDialog<T>({
@required BuildContext context, @required BuildContext context,
...@@ -639,10 +596,25 @@ Future<T> showDialog<T>({ ...@@ -639,10 +596,25 @@ Future<T> showDialog<T>({
WidgetBuilder builder, WidgetBuilder builder,
}) { }) {
assert(child == null || builder == null); assert(child == null || builder == null);
return Navigator.of(context, rootNavigator: true).push(new _DialogRoute<T>( return showGeneralDialog(
child: child ?? new Builder(builder: builder), context: context,
theme: Theme.of(context, shadowThemeOnly: true), pageBuilder: (BuildContext buildContext, Animation<double> animation, Animation<double> secondaryAnimation) {
final ThemeData theme = Theme.of(context, shadowThemeOnly: true);
final Widget pageChild = child ?? new Builder(builder: builder);
return new SafeArea(
child: new Builder(
builder: (BuildContext context) {
return theme != null
? new Theme(data: theme, child: pageChild)
: pageChild;
}
),
);
},
barrierDismissible: barrierDismissible, barrierDismissible: barrierDismissible,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
)); barrierColor: Colors.black54,
} transitionDuration: const Duration(milliseconds: 150),
transitionBuilder: _buildMaterialDialogTransitions,
);
}
\ No newline at end of file
...@@ -44,18 +44,6 @@ abstract class PageRoute<T> extends ModalRoute<T> { ...@@ -44,18 +44,6 @@ abstract class PageRoute<T> extends ModalRoute<T> {
} }
} }
/// Signature for the [PageRouteBuilder] function that builds the route's
/// primary contents.
///
/// See [ModalRoute.buildPage] for complete definition of the parameters.
typedef Widget RoutePageBuilder(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
/// Signature for the [PageRouteBuilder] function that builds the route's
/// transitions.
///
/// See [ModalRoute.buildTransitions] for complete definition of the parameters.
typedef Widget RouteTransitionsBuilder(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);
Widget _defaultTransitionsBuilder(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { Widget _defaultTransitionsBuilder(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return child; return child;
} }
......
...@@ -1379,3 +1379,143 @@ abstract class RouteAware { ...@@ -1379,3 +1379,143 @@ abstract class RouteAware {
/// longer visible. /// longer visible.
void didPushNext() { } void didPushNext() { }
} }
class _DialogRoute<T> extends PopupRoute<T> {
_DialogRoute({
@required RoutePageBuilder pageBuilder,
bool barrierDismissible = true,
String barrierLabel,
Color barrierColor = const Color(0x80000000),
Duration transitionDuration = const Duration(milliseconds: 200),
RouteTransitionsBuilder transitionBuilder,
RouteSettings settings,
}) : assert(barrierDismissible != null),
_pageBuilder = pageBuilder,
_barrierDismissible = barrierDismissible,
_barrierLabel = barrierLabel,
_barrierColor = barrierColor,
_transitionDuration = transitionDuration,
_transitionBuilder = transitionBuilder,
super(settings: settings);
final RoutePageBuilder _pageBuilder;
@override
bool get barrierDismissible => _barrierDismissible;
final bool _barrierDismissible;
@override
String get barrierLabel => _barrierLabel;
final String _barrierLabel;
@override
Color get barrierColor => _barrierColor;
final Color _barrierColor;
@override
Duration get transitionDuration => _transitionDuration;
final Duration _transitionDuration;
final RouteTransitionsBuilder _transitionBuilder;
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return new Semantics(
child: _pageBuilder(context, animation, secondaryAnimation),
scopesRoute: true,
explicitChildNodes: true,
);
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
if (_transitionBuilder == null) {
return new FadeTransition(
opacity: new CurvedAnimation(
parent: animation,
curve: Curves.linear,
),
child: child);
} // Some default transition
return _transitionBuilder(context, animation, secondaryAnimation, child);
}
}
/// Displays a dialog above the current contents of the app.
///
/// This function allows for customization of aspects of the dialog popup.
///
/// This function takes a `pageBuilder` which is used to build the primary
/// content of the route (typically a dialog widget). Content below the dialog
/// is dimmed with a [ModalBarrier]. The widget returned by the `pageBuilder`
/// does not share a context with the location that `showGeneralDialog` is
/// originally called from. Use a [StatefulBuilder] or a custom
/// [StatefulWidget] if the dialog needs to update dynamically. The
/// `pageBuilder` argument can not be null.
///
/// 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 `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".
///
/// The `barrierColor` argument is the color used for the modal barrier. This
/// argument defaults to `Color(0x80000000)`.
///
/// The `transitionDuration` argument is used to determine how long it takes
/// for the route to arrive on or leave off the screen. This argument defaults
/// to 200 milliseconds.
///
/// The `transitionBuilder` argument is used to define how the route arrives on
/// and leaves off the screen. By default, the transition is a linear fade of
/// the page's contents.
///
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
/// 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)`.
///
/// See also:
/// * [showDialog], which displays a Material-style dialog.
/// * [showCupertinoDialog], which displays an iOS-style dialog.
Future<T> showGeneralDialog<T>({
@required BuildContext context,
@required RoutePageBuilder pageBuilder,
bool barrierDismissible,
String barrierLabel,
Color barrierColor,
Duration transitionDuration,
RouteTransitionsBuilder transitionBuilder,
}) {
assert(pageBuilder != null);
assert(!barrierDismissible || barrierLabel != null);
return Navigator.of(context, rootNavigator: true).push(new _DialogRoute<T>(
pageBuilder: pageBuilder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
barrierColor: barrierColor,
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
));
}
/// Signature for the function that builds a route's primary contents.
/// Used in [PageRouteBuilder] and [showGeneralDialog].
///
/// See [ModalRoute.buildPage] for complete definition of the parameters.
typedef Widget RoutePageBuilder(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
/// Signature for the function that builds a route's transitions.
/// Used in [PageRouteBuilder] and [showGeneralDialog].
///
/// See [ModalRoute.buildTransitions] for complete definition of the parameters.
typedef Widget RouteTransitionsBuilder(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);
...@@ -332,6 +332,176 @@ void main() { ...@@ -332,6 +332,176 @@ void main() {
expect(scrollController.offset, 0.0); expect(scrollController.offset, 0.0);
expect(find.widgetWithText(CupertinoDialogAction, 'One'), findsNothing); expect(find.widgetWithText(CupertinoDialogAction, 'One'), findsNothing);
}); });
testWidgets('ScaleTransition animation for showCupertinoDialog()', (WidgetTester tester) async {
await tester.pumpWidget(
new CupertinoApp(
home: new Center(
child: new Builder(
builder: (BuildContext context) {
return new CupertinoButton(
onPressed: () {
showCupertinoDialog<void>(
context: context,
builder: (BuildContext context) {
return new CupertinoAlertDialog(
title: const Text('The title'),
content: const Text('The content'),
actions: <Widget>[
const CupertinoDialogAction(
child: Text('Cancel'),
),
new CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context);
},
child: const Text('Delete'),
),
],
);
},
);
},
child: const Text('Go'),
);
},
),
),
),
);
await tester.tap(find.text('Go'));
// Enter animation.
await tester.pump();
Transform transform = tester.widget(find.byType(Transform));
expect(transform.transform[0], closeTo(1.2, 0.01));
await tester.pump(const Duration(milliseconds: 50));
transform = tester.widget(find.byType(Transform));
expect(transform.transform[0], closeTo(1.182, 0.001));
await tester.pump(const Duration(milliseconds: 50));
transform = tester.widget(find.byType(Transform));
expect(transform.transform[0], closeTo(1.108, 0.001));
await tester.pump(const Duration(milliseconds: 50));
transform = tester.widget(find.byType(Transform));
expect(transform.transform[0], closeTo(1.044, 0.001));
await tester.pump(const Duration(milliseconds: 50));
transform = tester.widget(find.byType(Transform));
expect(transform.transform[0], closeTo(1.015, 0.001));
await tester.pump(const Duration(milliseconds: 50));
transform = tester.widget(find.byType(Transform));
expect(transform.transform[0], closeTo(1.003, 0.001));
await tester.pump(const Duration(milliseconds: 50));
transform = tester.widget(find.byType(Transform));
expect(transform.transform[0], closeTo(1.000, 0.001));
await tester.tap(find.text('Delete'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
// No scaling on exit animation.
expect(find.byType(Transform), findsNothing);
});
testWidgets('FadeTransition animation for showCupertinoDialog()', (WidgetTester tester) async {
await tester.pumpWidget(
new CupertinoApp(
home: new Center(
child: new Builder(
builder: (BuildContext context) {
return new CupertinoButton(
onPressed: () {
showCupertinoDialog<void>(
context: context,
builder: (BuildContext context) {
return new CupertinoAlertDialog(
title: const Text('The title'),
content: const Text('The content'),
actions: <Widget>[
const CupertinoDialogAction(
child: Text('Cancel'),
),
new CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context);
},
child: const Text('Delete'),
),
],
);
},
);
},
child: const Text('Go'),
);
},
),
),
),
);
await tester.tap(find.text('Go'));
// Enter animation.
await tester.pump();
FadeTransition transition = tester.firstWidget(find.byType(FadeTransition));
await tester.pump(const Duration(milliseconds: 25));
transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, closeTo(0.10, 0.001));
await tester.pump(const Duration(milliseconds: 25));
transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, closeTo(0.156, 0.001));
await tester.pump(const Duration(milliseconds: 25));
transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, closeTo(0.324, 0.001));
await tester.pump(const Duration(milliseconds: 25));
transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, closeTo(0.606, 0.001));
await tester.pump(const Duration(milliseconds: 25));
transition = tester.firstWidget(find.byType(FadeTransition));
expect(transition.opacity.value, closeTo(1.0, 0.001));
await tester.tap(find.text('Delete'));
// Exit animation, look at reverse FadeTransition.
await tester.pump(const Duration(milliseconds: 25));
transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1);
expect(transition.opacity.value, closeTo(0.358, 0.001));
await tester.pump(const Duration(milliseconds: 25));
transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1);
expect(transition.opacity.value, closeTo(0.231, 0.001));
await tester.pump(const Duration(milliseconds: 25));
transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1);
expect(transition.opacity.value, closeTo(0.128, 0.001));
await tester.pump(const Duration(milliseconds: 25));
transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1);
expect(transition.opacity.value, closeTo(0.056, 0.001));
await tester.pump(const Duration(milliseconds: 25));
transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1);
expect(transition.opacity.value, closeTo(0.013, 0.001));
await tester.pump(const Duration(milliseconds: 25));
transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1);
expect(transition.opacity.value, closeTo(0.0, 0.001));
});
} }
Widget boilerplate(Widget child) { Widget boilerplate(Widget child) {
......
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