Unverified Commit c6a428d5 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

[State Restoration] CupertinoModalPopupRoute (#74805)

* Expose CupertinoModalPopupRoute for state restoration

* Add state restoration test, expose kCupertinoModalBarrierColor
parent 6ea2806b
...@@ -25,9 +25,10 @@ const int _kMaxDroppedSwipePageForwardAnimationTime = 800; // Milliseconds. ...@@ -25,9 +25,10 @@ const int _kMaxDroppedSwipePageForwardAnimationTime = 800; // Milliseconds.
// user releases a page mid swipe. // user releases a page mid swipe.
const int _kMaxPageBackAnimationTime = 300; // Milliseconds. const int _kMaxPageBackAnimationTime = 300; // Milliseconds.
// Barrier color for a Cupertino modal barrier. /// Barrier color for a Cupertino modal barrier.
// Extracted from https://developer.apple.com/design/resources/. ///
const Color _kModalBarrierColor = CupertinoDynamicColor.withBrightness( /// Extracted from https://developer.apple.com/design/resources/.
const Color kCupertinoModalBarrierColor = CupertinoDynamicColor.withBrightness(
color: Color(0x33000000), color: Color(0x33000000),
darkColor: Color(0x7A000000), darkColor: Color(0x7A000000),
); );
...@@ -927,14 +928,46 @@ class _CupertinoEdgeShadowPainter extends BoxPainter { ...@@ -927,14 +928,46 @@ class _CupertinoEdgeShadowPainter extends BoxPainter {
} }
} }
class _CupertinoModalPopupRoute<T> extends PopupRoute<T> { /// A route that shows a modal iOS-style popup that slides up from the
_CupertinoModalPopupRoute({ /// bottom of the screen.
required this.barrierColor, ///
required this.barrierLabel, /// Such a popup is an alternative to a menu or a dialog and prevents the user
/// from interacting with the rest of the app.
///
/// It is used internally by [showCupertinoModalPopup] or can be directly pushed
/// onto the [Navigator] stack to enable state restoration. See
/// [showCupertinoModalPopup] for a state restoration app example.
///
/// The `barrierColor` argument determines the [Color] of the barrier underneath
/// the popup. When unspecified, the barrier color defaults to a light opacity
/// black scrim based on iOS's dialog screens. To correctly have iOS resolve
/// to the appropriate modal colors, pass in
/// `CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context)`.
///
/// The `barrierDismissible` argument determines whether clicking outside the
/// popup results in dismissal. It is `true` by default.
///
/// The `semanticsDismissible` argument is used to determine whether the
/// semantics of the modal barrier are included in the semantics tree.
///
/// The `routeSettings` argument is used to provide [RouteSettings] to the
/// created Route.
///
/// See also:
///
/// * [CupertinoActionSheet], which is the widget usually returned by the
/// `builder` argument.
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
class CupertinoModalPopupRoute<T> extends PopupRoute<T> {
/// A route that shows a modal iOS-style popup that slides up from the
/// bottom of the screen.
CupertinoModalPopupRoute({
required this.builder, required this.builder,
bool? barrierDismissible, this.barrierLabel = 'Dismiss',
this.barrierColor = kCupertinoModalBarrierColor,
bool barrierDismissible = true,
bool? semanticsDismissible, bool? semanticsDismissible,
required ImageFilter? filter, ImageFilter? filter,
RouteSettings? settings, RouteSettings? settings,
}) : super( }) : super(
filter: filter, filter: filter,
...@@ -944,6 +977,14 @@ class _CupertinoModalPopupRoute<T> extends PopupRoute<T> { ...@@ -944,6 +977,14 @@ class _CupertinoModalPopupRoute<T> extends PopupRoute<T> {
_semanticsDismissible = semanticsDismissible; _semanticsDismissible = semanticsDismissible;
} }
/// A builder that builds the widget tree for the [CupertinoModalPopupRoute].
///
/// The `builder` argument typically builds a [CupertinoActionSheet] widget.
///
/// Content below the widget is dimmed with a [ModalBarrier]. The widget built
/// by the `builder` does not share a context with the route it was originally
/// built from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the
/// widget needs to update dynamically.
final WidgetBuilder builder; final WidgetBuilder builder;
bool? _barrierDismissible; bool? _barrierDismissible;
...@@ -1043,6 +1084,87 @@ class _CupertinoModalPopupRoute<T> extends PopupRoute<T> { ...@@ -1043,6 +1084,87 @@ class _CupertinoModalPopupRoute<T> extends PopupRoute<T> {
/// Returns a `Future` that resolves to the value that was passed to /// Returns a `Future` that resolves to the value that was passed to
/// [Navigator.pop] when the popup was closed. /// [Navigator.pop] when the popup was closed.
/// ///
/// ### State Restoration in Modals
///
/// Using this method will not enable state restoration for the modal. In order
/// to enable state restoration for a modal, use [Navigator.restorablePush]
/// or [Navigator.restorablePushNamed] with [CupertinoModalPopupRoute].
///
/// For more information about state restoration, see [RestorationManager].
///
/// {@tool sample --template=freeform}
///
/// This sample demonstrates how to create a restorable Cupertino modal route.
/// This is accomplished by enabling state restoration by specifying
/// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to
/// push [CupertinoModalPopupRoute] when the [CupertinoButton] is tapped.
///
/// {@macro flutter.widgets.RestorationManager}
///
/// ```dart imports
/// import 'package:flutter/cupertino.dart';
/// ```
///
/// ```dart
/// void main() {
/// runApp(MyApp());
/// }
///
/// class MyApp extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return CupertinoApp(
/// restorationScopeId: 'app',
/// home: MyHomePage(),
/// );
/// }
/// }
///
/// class MyHomePage extends StatelessWidget {
/// static Route _modalBuilder(BuildContext context, Object? arguments) {
/// return CupertinoModalPopupRoute(
/// builder: (BuildContext context) {
/// return CupertinoActionSheet(
/// title: const Text('Title'),
/// message: const Text('Message'),
/// actions: [
/// CupertinoActionSheetAction(
/// child: const Text('Action One'),
/// onPressed: () {
/// Navigator.pop(context);
/// },
/// ),
/// CupertinoActionSheetAction(
/// child: const Text('Action Two'),
/// onPressed: () {
/// Navigator.pop(context);
/// },
/// ),
/// ],
/// );
/// },
/// );
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return CupertinoPageScaffold(
/// navigationBar: const CupertinoNavigationBar(
/// middle: Text('Home'),
/// ),
/// child: Center(child: CupertinoButton(
/// onPressed: () {
/// Navigator.of(context).restorablePush(_modalBuilder);
/// },
/// child: const Text('Open Modal'),
/// )),
/// );
/// }
/// }
/// ```
///
/// {@end-tool}
///
/// See also: /// See also:
/// ///
/// * [CupertinoActionSheet], which is the widget usually returned by the /// * [CupertinoActionSheet], which is the widget usually returned by the
...@@ -1052,7 +1174,7 @@ Future<T?> showCupertinoModalPopup<T>({ ...@@ -1052,7 +1174,7 @@ Future<T?> showCupertinoModalPopup<T>({
required BuildContext context, required BuildContext context,
required WidgetBuilder builder, required WidgetBuilder builder,
ImageFilter? filter, ImageFilter? filter,
Color barrierColor = _kModalBarrierColor, Color barrierColor = kCupertinoModalBarrierColor,
bool barrierDismissible = true, bool barrierDismissible = true,
bool useRootNavigator = true, bool useRootNavigator = true,
bool? semanticsDismissible, bool? semanticsDismissible,
...@@ -1060,12 +1182,11 @@ Future<T?> showCupertinoModalPopup<T>({ ...@@ -1060,12 +1182,11 @@ Future<T?> showCupertinoModalPopup<T>({
}) { }) {
assert(useRootNavigator != null); assert(useRootNavigator != null);
return Navigator.of(context, rootNavigator: useRootNavigator).push( return Navigator.of(context, rootNavigator: useRootNavigator).push(
_CupertinoModalPopupRoute<T>( CupertinoModalPopupRoute<T>(
barrierColor: CupertinoDynamicColor.resolve(barrierColor, context),
barrierDismissible: barrierDismissible,
barrierLabel: 'Dismiss',
builder: builder, builder: builder,
filter: filter, filter: filter,
barrierColor: CupertinoDynamicColor.resolve(barrierColor, context),
barrierDismissible: barrierDismissible,
semanticsDismissible: semanticsDismissible, semanticsDismissible: semanticsDismissible,
settings: routeSettings, settings: routeSettings,
), ),
...@@ -1219,7 +1340,7 @@ Future<T?> showCupertinoDialog<T>({ ...@@ -1219,7 +1340,7 @@ Future<T?> showCupertinoDialog<T>({
context: context, context: context,
barrierDismissible: barrierDismissible, barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel, barrierLabel: barrierLabel,
barrierColor: CupertinoDynamicColor.resolve(_kModalBarrierColor, context), barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
settings: routeSettings, settings: routeSettings,
)); ));
} }
...@@ -1276,7 +1397,7 @@ class CupertinoDialogRoute<T> extends RawDialogRoute<T> { ...@@ -1276,7 +1397,7 @@ class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
}, },
barrierDismissible: barrierDismissible, barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel, barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(_kModalBarrierColor, context), barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
transitionDuration: transitionDuration, transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder, transitionBuilder: transitionBuilder,
settings: settings, settings: settings,
......
...@@ -1684,6 +1684,36 @@ void main() { ...@@ -1684,6 +1684,36 @@ void main() {
navigator.removeRoute(r); navigator.removeRoute(r);
await tester.pump(); await tester.pump();
}); });
testWidgets('CupertinoModalPopupRoute is state restorable', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
restorationScopeId: 'app',
home: _RestorableModalTestWidget(),
),
);
expect(find.byType(CupertinoActionSheet), findsNothing);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.byType(CupertinoActionSheet), findsOneWidget);
final TestRestorationData restorationData = await tester.getRestorationData();
await tester.restartAndRestore();
expect(find.byType(CupertinoActionSheet), findsOneWidget);
// Tap on the barrier.
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
expect(find.byType(CupertinoActionSheet), findsNothing);
await tester.restoreFrom(restorationData);
expect(find.byType(CupertinoActionSheet), findsOneWidget);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
} }
class MockNavigatorObserver extends NavigatorObserver { class MockNavigatorObserver extends NavigatorObserver {
...@@ -1722,7 +1752,7 @@ class PopupObserver extends NavigatorObserver { ...@@ -1722,7 +1752,7 @@ class PopupObserver extends NavigatorObserver {
@override @override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_CupertinoModalPopupRoute')) { if (route is CupertinoModalPopupRoute) {
popupCount++; popupCount++;
} }
super.didPush(route, previousRoute); super.didPush(route, previousRoute);
...@@ -1746,7 +1776,7 @@ class RouteSettingsObserver extends NavigatorObserver { ...@@ -1746,7 +1776,7 @@ class RouteSettingsObserver extends NavigatorObserver {
@override @override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_CupertinoModalPopupRoute')) { if (route is CupertinoModalPopupRoute) {
routeName = route.settings.name; routeName = route.settings.name;
} }
super.didPush(route, previousRoute); super.didPush(route, previousRoute);
...@@ -1868,3 +1898,45 @@ class _TestPostRouteCancelState extends State<_TestPostRouteCancel> { ...@@ -1868,3 +1898,45 @@ class _TestPostRouteCancelState extends State<_TestPostRouteCancel> {
); );
} }
} }
class _RestorableModalTestWidget extends StatelessWidget{
static Route<void> _modalBuilder(BuildContext context, Object? arguments) {
return CupertinoModalPopupRoute<void>(
builder: (BuildContext context) {
return CupertinoActionSheet(
title: const Text('Title'),
message: const Text('Message'),
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('Action One'),
onPressed: () {
Navigator.pop(context);
},
),
CupertinoActionSheetAction(
child: const Text('Action Two'),
onPressed: () {
Navigator.pop(context);
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Home'),
),
child: Center(child: CupertinoButton(
onPressed: () {
Navigator.of(context).restorablePush(_modalBuilder);
},
child: const Text('X'),
)),
);
}
}
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