1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'dart:ui';
6///
7/// @docImport 'package:flutter/material.dart';
8/// @docImport 'package:flutter/services.dart';
9///
10/// @docImport 'app.dart';
11/// @docImport 'button.dart';
12/// @docImport 'dialog.dart';
13/// @docImport 'nav_bar.dart';
14/// @docImport 'page_scaffold.dart';
15/// @docImport 'tab_scaffold.dart';
16library;
17
18import 'dart:math';
19import 'dart:ui' show ImageFilter;
20
21import 'package:flutter/foundation.dart';
22import 'package:flutter/gestures.dart';
23import 'package:flutter/physics.dart';
24import 'package:flutter/rendering.dart';
25import 'package:flutter/widgets.dart';
26
27import 'colors.dart';
28import 'interface_level.dart';
29import 'localizations.dart';
30
31const double _kBackGestureWidth = 20.0;
32const double _kMinFlingVelocity = 1.0; // Screen widths per second.
33
34// The duration for a page to animate when the user releases it mid-swipe.
35const Duration _kDroppedSwipePageAnimationDuration = Duration(milliseconds: 350);
36
37/// Barrier color used for a barrier visible during transitions for Cupertino
38/// page routes.
39///
40/// This barrier color is only used for full-screen page routes with
41/// `fullscreenDialog: false`.
42///
43/// By default, `fullscreenDialog` Cupertino route transitions have no
44/// `barrierColor`, and [CupertinoDialogRoute]s and [CupertinoModalPopupRoute]s
45/// have a `barrierColor` defined by [kCupertinoModalBarrierColor].
46///
47/// A relatively rigorous eyeball estimation.
48const Color _kCupertinoPageTransitionBarrierColor = Color(0x18000000);
49
50/// Barrier color for a Cupertino modal barrier.
51///
52/// Extracted from https://developer.apple.com/design/resources/.
53const Color kCupertinoModalBarrierColor = CupertinoDynamicColor.withBrightness(
54 color: Color(0x33000000),
55 darkColor: Color(0x7A000000),
56);
57
58// The duration of the transition used when a modal popup is shown.
59const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335);
60
61// Offset from offscreen to the right to fully on screen.
62final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
63 begin: const Offset(1.0, 0.0),
64 end: Offset.zero,
65);
66
67// Offset from fully on screen to 1/3 offscreen to the left.
68final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>(
69 begin: Offset.zero,
70 end: const Offset(-1.0 / 3.0, 0.0),
71);
72
73// Offset from offscreen below to fully on screen.
74final Animatable<Offset> _kBottomUpTween = Tween<Offset>(
75 begin: const Offset(0.0, 1.0),
76 end: Offset.zero,
77);
78
79/// A mixin that replaces the entire screen with an iOS transition for a
80/// [PageRoute].
81///
82/// {@template flutter.cupertino.cupertinoRouteTransitionMixin}
83/// The page slides in from the right and exits in reverse. The page also shifts
84/// to the left in parallax when another page enters to cover it.
85///
86/// The page slides in from the bottom and exits in reverse with no parallax
87/// effect for fullscreen dialogs.
88/// {@endtemplate}
89///
90/// See also:
91///
92/// * [MaterialRouteTransitionMixin], which is a mixin that provides
93/// platform-appropriate transitions for a [PageRoute].
94/// * [CupertinoPageRoute], which is a [PageRoute] that leverages this mixin.
95mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
96 /// Builds the primary contents of the route.
97 @protected
98 Widget buildContent(BuildContext context);
99
100 /// {@template flutter.cupertino.CupertinoRouteTransitionMixin.title}
101 /// A title string for this route.
102 ///
103 /// Used to auto-populate [CupertinoNavigationBar] and
104 /// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when
105 /// one is not manually supplied.
106 /// {@endtemplate}
107 String? get title;
108
109 ValueNotifier<String?>? _previousTitle;
110
111 /// The title string of the previous [CupertinoPageRoute].
112 ///
113 /// The [ValueListenable]'s value is readable after the route is installed
114 /// onto a [Navigator]. The [ValueListenable] will also notify its listeners
115 /// if the value changes (such as by replacing the previous route).
116 ///
117 /// The [ValueListenable] itself will be null before the route is installed.
118 /// Its content value will be null if the previous route has no title or
119 /// is not a [CupertinoPageRoute].
120 ///
121 /// See also:
122 ///
123 /// * [ValueListenableBuilder], which can be used to listen and rebuild
124 /// widgets based on a ValueListenable.
125 ValueListenable<String?> get previousTitle {
126 assert(
127 _previousTitle != null,
128 'Cannot read the previousTitle for a route that has not yet been installed',
129 );
130 return _previousTitle!;
131 }
132
133 @override
134 void dispose() {
135 _previousTitle?.dispose();
136 super.dispose();
137 }
138
139 @override
140 void didChangePrevious(Route<dynamic>? previousRoute) {
141 final String? previousTitleString = previousRoute is CupertinoRouteTransitionMixin
142 ? previousRoute.title
143 : null;
144 if (_previousTitle == null) {
145 _previousTitle = ValueNotifier<String?>(previousTitleString);
146 } else {
147 _previousTitle!.value = previousTitleString;
148 }
149 super.didChangePrevious(previousRoute);
150 }
151
152 /// The duration of the page transition.
153 ///
154 /// A relatively rigorous eyeball estimation.
155 static const Duration kTransitionDuration = Duration(milliseconds: 500);
156
157 @override
158 Duration get transitionDuration => kTransitionDuration;
159
160 @override
161 Color? get barrierColor => fullscreenDialog ? null : _kCupertinoPageTransitionBarrierColor;
162
163 @override
164 String? get barrierLabel => null;
165
166 @override
167 bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
168 // Don't perform outgoing animation if the next route is a fullscreen dialog.
169 final bool nextRouteIsNotFullscreen =
170 (nextRoute is! PageRoute<T>) || !nextRoute.fullscreenDialog;
171
172 // If the next route has a delegated transition, then this route is able to
173 // use that delegated transition to smoothly sync with the next route's
174 // transition.
175 final bool nextRouteHasDelegatedTransition =
176 nextRoute is ModalRoute<T> && nextRoute.delegatedTransition != null;
177
178 // Otherwise if the next route has the same route transition mixin as this
179 // one, then this route will already be synced with its transition.
180 return nextRouteIsNotFullscreen &&
181 ((nextRoute is CupertinoRouteTransitionMixin) || nextRouteHasDelegatedTransition);
182 }
183
184 @override
185 bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) {
186 // Suppress previous route from transitioning if this is a fullscreenDialog route.
187 return previousRoute is PageRoute && !fullscreenDialog;
188 }
189
190 @override
191 Widget buildPage(
192 BuildContext context,
193 Animation<double> animation,
194 Animation<double> secondaryAnimation,
195 ) {
196 final Widget child = buildContent(context);
197 return Semantics(scopesRoute: true, explicitChildNodes: true, child: child);
198 }
199
200 // Called by _CupertinoBackGestureDetector when a pop ("back") drag start
201 // gesture is detected. The returned controller handles all of the subsequent
202 // drag events.
203 static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) {
204 assert(route.popGestureEnabled);
205
206 return _CupertinoBackGestureController<T>(
207 navigator: route.navigator!,
208 getIsCurrent: () => route.isCurrent,
209 getIsActive: () => route.isActive,
210 controller: route.controller!, // protected access
211 );
212 }
213
214 /// Returns a [CupertinoFullscreenDialogTransition] if [route] is a full
215 /// screen dialog, otherwise a [CupertinoPageTransition] is returned.
216 ///
217 /// Used by [CupertinoPageRoute.buildTransitions].
218 ///
219 /// This method can be applied to any [PageRoute], not just
220 /// [CupertinoPageRoute]. It's typically used to provide a Cupertino style
221 /// horizontal transition for material widgets when the target platform
222 /// is [TargetPlatform.iOS].
223 ///
224 /// See also:
225 ///
226 /// * [CupertinoPageTransitionsBuilder], which uses this method to define a
227 /// [PageTransitionsBuilder] for the [PageTransitionsTheme].
228 static Widget buildPageTransitions<T>(
229 PageRoute<T> route,
230 BuildContext context,
231 Animation<double> animation,
232 Animation<double> secondaryAnimation,
233 Widget child,
234 ) {
235 // Check if the route has an animation that's currently participating
236 // in a back swipe gesture.
237 //
238 // In the middle of a back gesture drag, let the transition be linear to
239 // match finger motions.
240 final bool linearTransition = route.popGestureInProgress;
241 if (route.fullscreenDialog) {
242 return CupertinoFullscreenDialogTransition(
243 primaryRouteAnimation: animation,
244 secondaryRouteAnimation: secondaryAnimation,
245 linearTransition: linearTransition,
246 child: child,
247 );
248 } else {
249 return CupertinoPageTransition(
250 primaryRouteAnimation: animation,
251 secondaryRouteAnimation: secondaryAnimation,
252 linearTransition: linearTransition,
253 child: _CupertinoBackGestureDetector<T>(
254 enabledCallback: () => route.popGestureEnabled,
255 onStartPopGesture: () => _startPopGesture<T>(route),
256 child: child,
257 ),
258 );
259 }
260 }
261
262 @override
263 Widget buildTransitions(
264 BuildContext context,
265 Animation<double> animation,
266 Animation<double> secondaryAnimation,
267 Widget child,
268 ) {
269 return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child);
270 }
271}
272
273/// A modal route that replaces the entire screen with an iOS transition.
274///
275/// {@macro flutter.cupertino.cupertinoRouteTransitionMixin}
276///
277/// By default, when a modal route is replaced by another, the previous route
278/// remains in memory. To free all the resources when this is not necessary, set
279/// [maintainState] to false.
280///
281/// The type `T` specifies the return type of the route which can be supplied as
282/// the route is popped from the stack via [Navigator.pop] when an optional
283/// `result` can be provided.
284///
285/// If `barrierDismissible` is true, then pressing the escape key on the keyboard
286/// will cause the current route to be popped with null as the value.
287///
288/// See also:
289///
290/// * [CupertinoRouteTransitionMixin], for a mixin that provides iOS transition
291/// for this modal route.
292/// * [MaterialPageRoute], for an adaptive [PageRoute] that uses a
293/// platform-appropriate transition.
294/// * [CupertinoPageScaffold], for applications that have one page with a fixed
295/// navigation bar on top.
296/// * [CupertinoTabScaffold], for applications that have a tab bar at the
297/// bottom with multiple pages.
298/// * [CupertinoPage], for a [Page] version of this class.
299class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> {
300 /// Creates a page route for use in an iOS designed app.
301 ///
302 /// The [builder], [maintainState], and [fullscreenDialog] arguments must not
303 /// be null.
304 CupertinoPageRoute({
305 required this.builder,
306 this.title,
307 super.settings,
308 super.requestFocus,
309 this.maintainState = true,
310 super.fullscreenDialog,
311 super.allowSnapshotting = true,
312 super.barrierDismissible = false,
313 }) {
314 assert(opaque);
315 }
316
317 @override
318 DelegatedTransitionBuilder? get delegatedTransition =>
319 CupertinoPageTransition.delegatedTransition;
320
321 /// Builds the primary contents of the route.
322 final WidgetBuilder builder;
323
324 @override
325 Widget buildContent(BuildContext context) => builder(context);
326
327 @override
328 final String? title;
329
330 @override
331 final bool maintainState;
332
333 @override
334 String get debugLabel => '${super.debugLabel}(${settings.name})';
335}
336
337// A page-based version of CupertinoPageRoute.
338//
339// This route uses the builder from the page to build its content. This ensures
340// the content is up to date after page updates.
341class _PageBasedCupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> {
342 _PageBasedCupertinoPageRoute({required CupertinoPage<T> page, super.allowSnapshotting = true})
343 : super(settings: page) {
344 assert(opaque);
345 }
346
347 @override
348 DelegatedTransitionBuilder? get delegatedTransition =>
349 fullscreenDialog ? null : CupertinoPageTransition.delegatedTransition;
350
351 CupertinoPage<T> get _page => settings as CupertinoPage<T>;
352
353 @override
354 Widget buildContent(BuildContext context) => _page.child;
355
356 @override
357 String? get title => _page.title;
358
359 @override
360 bool get maintainState => _page.maintainState;
361
362 @override
363 bool get fullscreenDialog => _page.fullscreenDialog;
364
365 @override
366 String get debugLabel => '${super.debugLabel}(${_page.name})';
367}
368
369/// A page that creates a cupertino style [PageRoute].
370///
371/// {@macro flutter.cupertino.cupertinoRouteTransitionMixin}
372///
373/// By default, when a created modal route is replaced by another, the previous
374/// route remains in memory. To free all the resources when this is not
375/// necessary, set [maintainState] to false.
376///
377/// The type `T` specifies the return type of the route which can be supplied as
378/// the route is popped from the stack via [Navigator.transitionDelegate] by
379/// providing the optional `result` argument to the
380/// [RouteTransitionRecord.markForPop] in the [TransitionDelegate.resolve].
381///
382/// See also:
383///
384/// * [CupertinoPageRoute], for a [PageRoute] version of this class.
385class CupertinoPage<T> extends Page<T> {
386 /// Creates a cupertino page.
387 const CupertinoPage({
388 required this.child,
389 this.maintainState = true,
390 this.title,
391 this.fullscreenDialog = false,
392 this.allowSnapshotting = true,
393 super.canPop,
394 super.onPopInvoked,
395 super.key,
396 super.name,
397 super.arguments,
398 super.restorationId,
399 });
400
401 /// The content to be shown in the [Route] created by this page.
402 final Widget child;
403
404 /// {@macro flutter.cupertino.CupertinoRouteTransitionMixin.title}
405 final String? title;
406
407 /// {@macro flutter.widgets.ModalRoute.maintainState}
408 final bool maintainState;
409
410 /// {@macro flutter.widgets.PageRoute.fullscreenDialog}
411 final bool fullscreenDialog;
412
413 /// {@macro flutter.widgets.TransitionRoute.allowSnapshotting}
414 final bool allowSnapshotting;
415
416 @override
417 Route<T> createRoute(BuildContext context) {
418 return _PageBasedCupertinoPageRoute<T>(page: this, allowSnapshotting: allowSnapshotting);
419 }
420}
421
422/// Provides an iOS-style page transition animation.
423///
424/// The page slides in from the right and exits in reverse. It also shifts to the left in
425/// a parallax motion when another page enters to cover it.
426class CupertinoPageTransition extends StatefulWidget {
427 /// Creates an iOS-style page transition.
428 ///
429 const CupertinoPageTransition({
430 super.key,
431 required this.primaryRouteAnimation,
432 required this.secondaryRouteAnimation,
433 required this.child,
434 required this.linearTransition,
435 });
436
437 /// The widget below this widget in the tree.
438 final Widget child;
439
440 /// * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0
441 /// when this screen is being pushed.
442 final Animation<double> primaryRouteAnimation;
443
444 /// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0
445 /// when another screen is being pushed on top of this one.
446 final Animation<double> secondaryRouteAnimation;
447
448 /// * `linearTransition` is whether to perform the transitions linearly.
449 /// Used to precisely track back gesture drags.
450 final bool linearTransition;
451
452 /// The Cupertino styled [DelegatedTransitionBuilder] provided to the previous
453 /// route.
454 ///
455 /// {@macro flutter.widgets.delegatedTransition}
456 static Widget? delegatedTransition(
457 BuildContext context,
458 Animation<double> animation,
459 Animation<double> secondaryAnimation,
460 bool allowSnapshotting,
461 Widget? child,
462 ) {
463 final CurvedAnimation animation = CurvedAnimation(
464 parent: secondaryAnimation,
465 curve: Curves.linearToEaseOut,
466 reverseCurve: Curves.easeInToLinear,
467 );
468 final Animation<Offset> delegatedPositionAnimation = animation.drive(_kMiddleLeftTween);
469 animation.dispose();
470
471 assert(debugCheckHasDirectionality(context));
472 final TextDirection textDirection = Directionality.of(context);
473 return SlideTransition(
474 position: delegatedPositionAnimation,
475 textDirection: textDirection,
476 transformHitTests: false,
477 child: child,
478 );
479 }
480
481 @override
482 State<CupertinoPageTransition> createState() => _CupertinoPageTransitionState();
483}
484
485class _CupertinoPageTransitionState extends State<CupertinoPageTransition> {
486 // When this page is coming in to cover another page.
487 late Animation<Offset> _primaryPositionAnimation;
488 // When this page is becoming covered by another page.
489 late Animation<Offset> _secondaryPositionAnimation;
490 // Shadow of page which is coming in to cover another page.
491 late Animation<Decoration> _primaryShadowAnimation;
492 // Curve of primary page which is coming in to cover another page.
493 CurvedAnimation? _primaryPositionCurve;
494 // Curve of secondary page which is becoming covered by another page.
495 CurvedAnimation? _secondaryPositionCurve;
496 // Curve of primary page's shadow.
497 CurvedAnimation? _primaryShadowCurve;
498
499 @override
500 void initState() {
501 super.initState();
502 _setupAnimation();
503 }
504
505 @override
506 void didUpdateWidget(covariant CupertinoPageTransition oldWidget) {
507 super.didUpdateWidget(oldWidget);
508 if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation ||
509 oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation ||
510 oldWidget.linearTransition != widget.linearTransition) {
511 _disposeCurve();
512 _setupAnimation();
513 }
514 }
515
516 @override
517 void dispose() {
518 _disposeCurve();
519 super.dispose();
520 }
521
522 void _disposeCurve() {
523 _primaryPositionCurve?.dispose();
524 _secondaryPositionCurve?.dispose();
525 _primaryShadowCurve?.dispose();
526 _primaryPositionCurve = null;
527 _secondaryPositionCurve = null;
528 _primaryShadowCurve = null;
529 }
530
531 void _setupAnimation() {
532 if (!widget.linearTransition) {
533 _primaryPositionCurve = CurvedAnimation(
534 parent: widget.primaryRouteAnimation,
535 curve: Curves.fastEaseInToSlowEaseOut,
536 reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped,
537 );
538 _secondaryPositionCurve = CurvedAnimation(
539 parent: widget.secondaryRouteAnimation,
540 curve: Curves.linearToEaseOut,
541 reverseCurve: Curves.easeInToLinear,
542 );
543 _primaryShadowCurve = CurvedAnimation(
544 parent: widget.primaryRouteAnimation,
545 curve: Curves.linearToEaseOut,
546 );
547 }
548 _primaryPositionAnimation = (_primaryPositionCurve ?? widget.primaryRouteAnimation).drive(
549 _kRightMiddleTween,
550 );
551 _secondaryPositionAnimation = (_secondaryPositionCurve ?? widget.secondaryRouteAnimation).drive(
552 _kMiddleLeftTween,
553 );
554 _primaryShadowAnimation = (_primaryShadowCurve ?? widget.primaryRouteAnimation).drive(
555 _CupertinoEdgeShadowDecoration.kTween,
556 );
557 }
558
559 @override
560 Widget build(BuildContext context) {
561 assert(debugCheckHasDirectionality(context));
562 final TextDirection textDirection = Directionality.of(context);
563 return SlideTransition(
564 position: _secondaryPositionAnimation,
565 textDirection: textDirection,
566 transformHitTests: false,
567 child: SlideTransition(
568 position: _primaryPositionAnimation,
569 textDirection: textDirection,
570 child: DecoratedBoxTransition(decoration: _primaryShadowAnimation, child: widget.child),
571 ),
572 );
573 }
574}
575
576/// An iOS-style transition used for summoning fullscreen dialogs.
577///
578/// For example, used when creating a new calendar event by bringing in the next
579/// screen from the bottom.
580class CupertinoFullscreenDialogTransition extends StatefulWidget {
581 /// Creates an iOS-style transition used for summoning fullscreen dialogs.
582 ///
583 const CupertinoFullscreenDialogTransition({
584 super.key,
585 required this.primaryRouteAnimation,
586 required this.secondaryRouteAnimation,
587 required this.child,
588 required this.linearTransition,
589 });
590
591 /// * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0
592 /// when this screen is being pushed.
593 final Animation<double> primaryRouteAnimation;
594
595 /// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0
596 /// when another screen is being pushed on top of this one.
597 final Animation<double> secondaryRouteAnimation;
598
599 /// * `linearTransition` is whether to perform the transitions linearly.
600 /// Used to precisely track back gesture drags.
601 final bool linearTransition;
602
603 /// The widget below this widget in the tree.
604 final Widget child;
605
606 @override
607 State<CupertinoFullscreenDialogTransition> createState() =>
608 _CupertinoFullscreenDialogTransitionState();
609}
610
611class _CupertinoFullscreenDialogTransitionState extends State<CupertinoFullscreenDialogTransition> {
612 /// When this page is coming in to cover another page.
613 late Animation<Offset> _primaryPositionAnimation;
614
615 /// When this page is becoming covered by another page.
616 late Animation<Offset> _secondaryPositionAnimation;
617
618 /// Curve of primary page which is coming in to cover another page.
619 CurvedAnimation? _primaryPositionCurve;
620
621 /// Curve of secondary page which is becoming covered by another page.
622 CurvedAnimation? _secondaryPositionCurve;
623
624 @override
625 void initState() {
626 super.initState();
627 _setupAnimation();
628 }
629
630 @override
631 void didUpdateWidget(covariant CupertinoFullscreenDialogTransition oldWidget) {
632 super.didUpdateWidget(oldWidget);
633 if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation ||
634 oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation ||
635 oldWidget.linearTransition != widget.linearTransition) {
636 _disposeCurve();
637 _setupAnimation();
638 }
639 }
640
641 @override
642 void dispose() {
643 _disposeCurve();
644 super.dispose();
645 }
646
647 void _disposeCurve() {
648 _primaryPositionCurve?.dispose();
649 _secondaryPositionCurve?.dispose();
650 _primaryPositionCurve = null;
651 _secondaryPositionCurve = null;
652 }
653
654 void _setupAnimation() {
655 _primaryPositionAnimation = (_primaryPositionCurve = CurvedAnimation(
656 parent: widget.primaryRouteAnimation,
657 curve: Curves.linearToEaseOut,
658 // The curve must be flipped so that the reverse animation doesn't play
659 // an ease-in curve, which iOS does not use.
660 reverseCurve: Curves.linearToEaseOut.flipped,
661 )).drive(_kBottomUpTween);
662 _secondaryPositionAnimation =
663 (widget.linearTransition
664 ? widget.secondaryRouteAnimation
665 : _secondaryPositionCurve = CurvedAnimation(
666 parent: widget.secondaryRouteAnimation,
667 curve: Curves.linearToEaseOut,
668 reverseCurve: Curves.easeInToLinear,
669 ))
670 .drive(_kMiddleLeftTween);
671 }
672
673 @override
674 Widget build(BuildContext context) {
675 assert(debugCheckHasDirectionality(context));
676 final TextDirection textDirection = Directionality.of(context);
677 return SlideTransition(
678 position: _secondaryPositionAnimation,
679 textDirection: textDirection,
680 transformHitTests: false,
681 child: SlideTransition(position: _primaryPositionAnimation, child: widget.child),
682 );
683 }
684}
685
686/// This is the widget side of [_CupertinoBackGestureController].
687///
688/// This widget provides a gesture recognizer which, when it determines the
689/// route can be closed with a back gesture, creates the controller and
690/// feeds it the input from the gesture recognizer.
691///
692/// The gesture data is converted from absolute coordinates to logical
693/// coordinates by this widget.
694///
695/// The type `T` specifies the return type of the route with which this gesture
696/// detector is associated.
697class _CupertinoBackGestureDetector<T> extends StatefulWidget {
698 const _CupertinoBackGestureDetector({
699 super.key,
700 required this.enabledCallback,
701 required this.onStartPopGesture,
702 required this.child,
703 });
704
705 final Widget child;
706
707 final ValueGetter<bool> enabledCallback;
708
709 final ValueGetter<_CupertinoBackGestureController<T>> onStartPopGesture;
710
711 @override
712 _CupertinoBackGestureDetectorState<T> createState() => _CupertinoBackGestureDetectorState<T>();
713}
714
715class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureDetector<T>> {
716 _CupertinoBackGestureController<T>? _backGestureController;
717
718 late HorizontalDragGestureRecognizer _recognizer;
719
720 @override
721 void initState() {
722 super.initState();
723 _recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
724 ..onStart = _handleDragStart
725 ..onUpdate = _handleDragUpdate
726 ..onEnd = _handleDragEnd
727 ..onCancel = _handleDragCancel;
728 }
729
730 @override
731 void dispose() {
732 _recognizer.dispose();
733
734 // If this is disposed during a drag, call navigator.didStopUserGesture.
735 if (_backGestureController != null) {
736 WidgetsBinding.instance.addPostFrameCallback((_) {
737 if (_backGestureController?.navigator.mounted ?? false) {
738 _backGestureController?.navigator.didStopUserGesture();
739 }
740 _backGestureController = null;
741 });
742 }
743 super.dispose();
744 }
745
746 void _handleDragStart(DragStartDetails details) {
747 assert(mounted);
748 assert(_backGestureController == null);
749 _backGestureController = widget.onStartPopGesture();
750 }
751
752 void _handleDragUpdate(DragUpdateDetails details) {
753 assert(mounted);
754 assert(_backGestureController != null);
755 _backGestureController!.dragUpdate(
756 _convertToLogical(details.primaryDelta! / context.size!.width),
757 );
758 }
759
760 void _handleDragEnd(DragEndDetails details) {
761 assert(mounted);
762 assert(_backGestureController != null);
763 _backGestureController!.dragEnd(
764 _convertToLogical(details.velocity.pixelsPerSecond.dx / context.size!.width),
765 );
766 _backGestureController = null;
767 }
768
769 void _handleDragCancel() {
770 assert(mounted);
771 // This can be called even if start is not called, paired with the "down" event
772 // that we don't consider here.
773 _backGestureController?.dragEnd(0.0);
774 _backGestureController = null;
775 }
776
777 void _handlePointerDown(PointerDownEvent event) {
778 if (widget.enabledCallback()) {
779 _recognizer.addPointer(event);
780 }
781 }
782
783 double _convertToLogical(double value) {
784 return switch (Directionality.of(context)) {
785 TextDirection.rtl => -value,
786 TextDirection.ltr => value,
787 };
788 }
789
790 @override
791 Widget build(BuildContext context) {
792 assert(debugCheckHasDirectionality(context));
793 // For devices with notches, the drag area needs to be larger on the side
794 // that has the notch.
795 final double dragAreaWidth = switch (Directionality.of(context)) {
796 TextDirection.rtl => MediaQuery.paddingOf(context).right,
797 TextDirection.ltr => MediaQuery.paddingOf(context).left,
798 };
799 return Stack(
800 fit: StackFit.passthrough,
801 children: <Widget>[
802 widget.child,
803 PositionedDirectional(
804 start: 0.0,
805 width: max(dragAreaWidth, _kBackGestureWidth),
806 top: 0.0,
807 bottom: 0.0,
808 child: Listener(onPointerDown: _handlePointerDown, behavior: HitTestBehavior.translucent),
809 ),
810 ],
811 );
812 }
813}
814
815/// A controller for an iOS-style back gesture.
816///
817/// This is created by a [CupertinoPageRoute] in response from a gesture caught
818/// by a [_CupertinoBackGestureDetector] widget, which then also feeds it input
819/// from the gesture. It controls the animation controller owned by the route,
820/// based on the input provided by the gesture detector.
821///
822/// This class works entirely in logical coordinates (0.0 is new page dismissed,
823/// 1.0 is new page on top).
824///
825/// The type `T` specifies the return type of the route with which this gesture
826/// detector controller is associated.
827class _CupertinoBackGestureController<T> {
828 /// Creates a controller for an iOS-style back gesture.
829 _CupertinoBackGestureController({
830 required this.navigator,
831 required this.controller,
832 required this.getIsActive,
833 required this.getIsCurrent,
834 }) {
835 navigator.didStartUserGesture();
836 }
837
838 final AnimationController controller;
839 final NavigatorState navigator;
840 final ValueGetter<bool> getIsActive;
841 final ValueGetter<bool> getIsCurrent;
842
843 /// The drag gesture has changed by [delta]. The total range of the drag
844 /// should be 0.0 to 1.0.
845 void dragUpdate(double delta) {
846 controller.value -= delta;
847 }
848
849 /// The drag gesture has ended with a horizontal motion of [velocity] as a
850 /// fraction of screen width per second.
851 void dragEnd(double velocity) {
852 // Fling in the appropriate direction.
853 //
854 // This curve has been determined through rigorously eyeballing native iOS
855 // animations.
856 const Curve animationCurve = Curves.fastEaseInToSlowEaseOut;
857 final bool isCurrent = getIsCurrent();
858 final bool animateForward;
859
860 if (!isCurrent) {
861 // If the page has already been navigated away from, then the animation
862 // direction depends on whether or not it's still in the navigation stack,
863 // regardless of velocity or drag position. For example, if a route is
864 // being slowly dragged back by just a few pixels, but then a programmatic
865 // pop occurs, the route should still be animated off the screen.
866 // See https://github.com/flutter/flutter/issues/141268.
867 animateForward = getIsActive();
868 } else if (velocity.abs() >= _kMinFlingVelocity) {
869 // If the user releases the page before mid screen with sufficient velocity,
870 // or after mid screen, we should animate the page out. Otherwise, the page
871 // should be animated back in.
872 animateForward = velocity <= 0;
873 } else {
874 animateForward = controller.value > 0.5;
875 }
876
877 if (animateForward) {
878 controller.animateTo(
879 1.0,
880 duration: _kDroppedSwipePageAnimationDuration,
881 curve: animationCurve,
882 );
883 } else {
884 if (isCurrent) {
885 // This route is destined to pop at this point. Reuse navigator's pop.
886 navigator.pop();
887 }
888
889 // The popping may have finished inline if already at the target destination.
890 if (controller.isAnimating) {
891 controller.animateBack(
892 0.0,
893 duration: _kDroppedSwipePageAnimationDuration,
894 curve: animationCurve,
895 );
896 }
897 }
898
899 if (controller.isAnimating) {
900 // Keep the userGestureInProgress in true state so we don't change the
901 // curve of the page transition mid-flight since CupertinoPageTransition
902 // depends on userGestureInProgress.
903 late AnimationStatusListener animationStatusCallback;
904 animationStatusCallback = (AnimationStatus status) {
905 navigator.didStopUserGesture();
906 controller.removeStatusListener(animationStatusCallback);
907 };
908 controller.addStatusListener(animationStatusCallback);
909 } else {
910 navigator.didStopUserGesture();
911 }
912 }
913}
914
915// A custom [Decoration] used to paint an extra shadow on the start edge of the
916// box it's decorating. It's like a [BoxDecoration] with only a gradient except
917// it paints on the start side of the box instead of behind the box.
918class _CupertinoEdgeShadowDecoration extends Decoration {
919 const _CupertinoEdgeShadowDecoration._([this._colors]);
920
921 static DecorationTween kTween = DecorationTween(
922 begin: const _CupertinoEdgeShadowDecoration._(), // No decoration initially.
923 end: const _CupertinoEdgeShadowDecoration._(
924 // Eyeballed gradient used to mimic a drop shadow on the start side only.
925 <Color>[Color(0x04000000), CupertinoColors.transparent],
926 ),
927 );
928
929 // Colors used to paint a gradient at the start edge of the box it is
930 // decorating.
931 //
932 // The first color in the list is used at the start of the gradient, which
933 // is located at the start edge of the decorated box.
934 //
935 // If this is null, no shadow is drawn.
936 //
937 // The list must have at least two colors in it (otherwise it would not be a
938 // gradient).
939 final List<Color>? _colors;
940
941 // Linearly interpolate between two edge shadow decorations decorations.
942 //
943 // The `t` argument represents position on the timeline, with 0.0 meaning
944 // that the interpolation has not started, returning `a` (or something
945 // equivalent to `a`), 1.0 meaning that the interpolation has finished,
946 // returning `b` (or something equivalent to `b`), and values in between
947 // meaning that the interpolation is at the relevant point on the timeline
948 // between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
949 // 1.0, so negative values and values greater than 1.0 are valid (and can
950 // easily be generated by curves such as [Curves.elasticInOut]).
951 //
952 // Values for `t` are usually obtained from an [Animation], such as
953 // an [AnimationController].
954 //
955 // See also:
956 //
957 // * [Decoration.lerp].
958 static _CupertinoEdgeShadowDecoration? lerp(
959 _CupertinoEdgeShadowDecoration? a,
960 _CupertinoEdgeShadowDecoration? b,
961 double t,
962 ) {
963 if (identical(a, b)) {
964 return a;
965 }
966 if (a == null) {
967 return b!._colors == null
968 ? b
969 : _CupertinoEdgeShadowDecoration._(
970 b._colors!.map<Color>((Color color) => Color.lerp(null, color, t)!).toList(),
971 );
972 }
973 if (b == null) {
974 return a._colors == null
975 ? a
976 : _CupertinoEdgeShadowDecoration._(
977 a._colors.map<Color>((Color color) => Color.lerp(null, color, 1.0 - t)!).toList(),
978 );
979 }
980 assert(b._colors != null || a._colors != null);
981 // If it ever becomes necessary, we could allow decorations with different
982 // length' here, similarly to how it is handled in [LinearGradient.lerp].
983 assert(b._colors == null || a._colors == null || a._colors.length == b._colors.length);
984 return _CupertinoEdgeShadowDecoration._(<Color>[
985 for (int i = 0; i < b._colors!.length; i += 1) Color.lerp(a._colors?[i], b._colors[i], t)!,
986 ]);
987 }
988
989 @override
990 _CupertinoEdgeShadowDecoration lerpFrom(Decoration? a, double t) {
991 if (a is _CupertinoEdgeShadowDecoration) {
992 return _CupertinoEdgeShadowDecoration.lerp(a, this, t)!;
993 }
994 return _CupertinoEdgeShadowDecoration.lerp(null, this, t)!;
995 }
996
997 @override
998 _CupertinoEdgeShadowDecoration lerpTo(Decoration? b, double t) {
999 if (b is _CupertinoEdgeShadowDecoration) {
1000 return _CupertinoEdgeShadowDecoration.lerp(this, b, t)!;
1001 }
1002 return _CupertinoEdgeShadowDecoration.lerp(this, null, t)!;
1003 }
1004
1005 @override
1006 _CupertinoEdgeShadowPainter createBoxPainter([VoidCallback? onChanged]) {
1007 return _CupertinoEdgeShadowPainter(this, onChanged);
1008 }
1009
1010 @override
1011 bool operator ==(Object other) {
1012 if (other.runtimeType != runtimeType) {
1013 return false;
1014 }
1015 return other is _CupertinoEdgeShadowDecoration && other._colors == _colors;
1016 }
1017
1018 @override
1019 int get hashCode => _colors.hashCode;
1020
1021 @override
1022 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1023 super.debugFillProperties(properties);
1024 properties.add(IterableProperty<Color>('colors', _colors));
1025 }
1026}
1027
1028/// A [BoxPainter] used to draw the page transition shadow using gradients.
1029class _CupertinoEdgeShadowPainter extends BoxPainter {
1030 _CupertinoEdgeShadowPainter(this._decoration, super.onChanged)
1031 : assert(_decoration._colors == null || _decoration._colors.length > 1);
1032
1033 final _CupertinoEdgeShadowDecoration _decoration;
1034
1035 @override
1036 void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
1037 final List<Color>? colors = _decoration._colors;
1038 if (colors == null) {
1039 return;
1040 }
1041
1042 // The following code simulates drawing a [LinearGradient] configured as
1043 // follows:
1044 //
1045 // LinearGradient(
1046 // begin: AlignmentDirectional(0.90, 0.0), // Spans 5% of the page.
1047 // colors: _decoration._colors,
1048 // )
1049 //
1050 // A performance evaluation on Feb 8, 2021 showed, that drawing the gradient
1051 // manually as implemented below is more performant than relying on
1052 // [LinearGradient.createShader] because compiling that shader takes a long
1053 // time. On an iPhone XR, the implementation below reduced the worst frame
1054 // time for a cupertino page transition of a newly installed app from ~95ms
1055 // down to ~30ms, mainly because there's no longer a need to compile a
1056 // shader for the LinearGradient.
1057 //
1058 // The implementation below divides the width of the shadow into multiple
1059 // bands of equal width, one for each color interval defined by
1060 // `_decoration._colors`. Band x is filled with a gradient going from
1061 // `_decoration._colors[x]` to `_decoration._colors[x + 1]` by drawing a
1062 // bunch of 1px wide rects. The rects change their color by lerping between
1063 // the two colors that define the interval of the band.
1064
1065 // Shadow spans 5% of the page.
1066 final double shadowWidth = 0.05 * configuration.size!.width;
1067 final double shadowHeight = configuration.size!.height;
1068 final double bandWidth = shadowWidth / (colors.length - 1);
1069
1070 final TextDirection? textDirection = configuration.textDirection;
1071 assert(textDirection != null);
1072 final (double shadowDirection, double start) = switch (textDirection!) {
1073 TextDirection.rtl => (1, offset.dx + configuration.size!.width),
1074 TextDirection.ltr => (-1, offset.dx),
1075 };
1076
1077 int bandColorIndex = 0;
1078 for (int dx = 0; dx < shadowWidth; dx += 1) {
1079 if (dx ~/ bandWidth != bandColorIndex) {
1080 bandColorIndex += 1;
1081 }
1082 final Paint paint = Paint()
1083 ..color = Color.lerp(
1084 colors[bandColorIndex],
1085 colors[bandColorIndex + 1],
1086 (dx % bandWidth) / bandWidth,
1087 )!;
1088 final double x = start + shadowDirection * dx;
1089 canvas.drawRect(Rect.fromLTWH(x - 1.0, offset.dy, 1.0, shadowHeight), paint);
1090 }
1091 }
1092}
1093
1094// The stiffness used by dialogs and action sheets.
1095//
1096// The stiffness value is obtained by examining the properties of
1097// `CASpringAnimation` in Xcode. The damping value is derived similarly, with
1098// additional precision calculated based on `_kStandardStiffness` to ensure a
1099// damping ratio of 1 (critically damped): damping = 2 * sqrt(stiffness)
1100const double _kStandardStiffness = 522.35;
1101const double _kStandardDamping = 45.7099552;
1102const SpringDescription _kStandardSpring = SpringDescription(
1103 mass: 1,
1104 stiffness: _kStandardStiffness,
1105 damping: _kStandardDamping,
1106);
1107// The iOS spring animation duration is 0.404 seconds, based on the properties
1108// of `CASpringAnimation` in Xcode. At this point, the spring's position
1109// `x(0.404)` is approximately 0.9990000, suggesting that iOS uses a position
1110// tolerance of 1e-3 (matching the default `_epsilonDefault` value).
1111//
1112// However, the spring's velocity `dx(0.404)` is about 0.02, indicating that iOS
1113// may not consider velocity when determining the animation's end condition. To
1114// account for this, a larger velocity tolerance is applied here for added
1115// safety.
1116const Tolerance _kStandardTolerance = Tolerance(velocity: 0.03);
1117
1118/// A route that shows a modal iOS-style popup that slides up from the
1119/// bottom of the screen.
1120///
1121/// Such a popup is an alternative to a menu or a dialog and prevents the user
1122/// from interacting with the rest of the app.
1123///
1124/// It is used internally by [showCupertinoModalPopup] or can be directly pushed
1125/// onto the [Navigator] stack to enable state restoration. See
1126/// [showCupertinoModalPopup] for a state restoration app example.
1127///
1128/// The `barrierColor` argument determines the [Color] of the barrier underneath
1129/// the popup. When unspecified, the barrier color defaults to a light opacity
1130/// black scrim based on iOS's dialog screens. To correctly have iOS resolve
1131/// to the appropriate modal colors, pass in
1132/// `CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context)`.
1133///
1134/// The `barrierDismissible` argument determines whether clicking outside the
1135/// popup results in dismissal. It is `true` by default.
1136///
1137/// The `semanticsDismissible` argument is used to determine whether the
1138/// semantics of the modal barrier are included in the semantics tree.
1139///
1140/// The `routeSettings` argument is used to provide [RouteSettings] to the
1141/// created Route.
1142///
1143/// {@macro flutter.widgets.RawDialogRoute}
1144///
1145/// See also:
1146///
1147/// * [DisplayFeatureSubScreen], which documents the specifics of how
1148/// [DisplayFeature]s can split the screen into sub-screens.
1149/// * [CupertinoActionSheet], which is the widget usually returned by the
1150/// `builder` argument.
1151/// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
1152class CupertinoModalPopupRoute<T> extends PopupRoute<T> {
1153 /// A route that shows a modal iOS-style popup that slides up from the
1154 /// bottom of the screen.
1155 CupertinoModalPopupRoute({
1156 required this.builder,
1157 this.barrierLabel = 'Dismiss',
1158 this.barrierColor = kCupertinoModalBarrierColor,
1159 bool barrierDismissible = true,
1160 bool semanticsDismissible = false,
1161 super.filter,
1162 super.settings,
1163 super.requestFocus,
1164 this.anchorPoint,
1165 }) : _barrierDismissible = barrierDismissible,
1166 _semanticsDismissible = semanticsDismissible;
1167
1168 /// A builder that builds the widget tree for the [CupertinoModalPopupRoute].
1169 ///
1170 /// The [builder] argument typically builds a [CupertinoActionSheet] widget.
1171 ///
1172 /// Content below the widget is dimmed with a [ModalBarrier]. The widget built
1173 /// by the [builder] does not share a context with the route it was originally
1174 /// built from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the
1175 /// widget needs to update dynamically.
1176 final WidgetBuilder builder;
1177
1178 final bool _barrierDismissible;
1179
1180 final bool _semanticsDismissible;
1181
1182 @override
1183 final String barrierLabel;
1184
1185 @override
1186 final Color? barrierColor;
1187
1188 @override
1189 bool get barrierDismissible => _barrierDismissible;
1190
1191 @override
1192 bool get semanticsDismissible => _semanticsDismissible;
1193
1194 @override
1195 Duration get transitionDuration => _kModalPopupTransitionDuration;
1196
1197 /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
1198 final Offset? anchorPoint;
1199
1200 @override
1201 Simulation createSimulation({required bool forward}) {
1202 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
1203 final double end = forward ? 1.0 : 0.0;
1204 return SpringSimulation(
1205 _kStandardSpring,
1206 controller!.value,
1207 end,
1208 0,
1209 tolerance: _kStandardTolerance,
1210 snapToEnd: true,
1211 );
1212 }
1213
1214 @override
1215 Widget buildPage(
1216 BuildContext context,
1217 Animation<double> animation,
1218 Animation<double> secondaryAnimation,
1219 ) {
1220 return CupertinoUserInterfaceLevel(
1221 data: CupertinoUserInterfaceLevelData.elevated,
1222 child: DisplayFeatureSubScreen(
1223 anchorPoint: anchorPoint,
1224 child: Builder(builder: builder),
1225 ),
1226 );
1227 }
1228
1229 @override
1230 Widget buildTransitions(
1231 BuildContext context,
1232 Animation<double> animation,
1233 Animation<double> secondaryAnimation,
1234 Widget child,
1235 ) {
1236 return Align(
1237 alignment: Alignment.bottomCenter,
1238 child: FractionalTranslation(translation: _offsetTween.evaluate(animation), child: child),
1239 );
1240 }
1241
1242 static final Tween<Offset> _offsetTween = Tween<Offset>(
1243 begin: const Offset(0.0, 1.0),
1244 end: Offset.zero,
1245 );
1246}
1247
1248/// Shows a modal iOS-style popup that slides up from the bottom of the screen.
1249///
1250/// Such a popup is an alternative to a menu or a dialog and prevents the user
1251/// from interacting with the rest of the app.
1252///
1253/// The `context` argument is used to look up the [Navigator] for the popup.
1254/// It is only used when the method is called. Its corresponding widget can be
1255/// safely removed from the tree before the popup is closed.
1256///
1257/// The `barrierColor` argument determines the [Color] of the barrier underneath
1258/// the popup. When unspecified, the barrier color defaults to a light opacity
1259/// black scrim based on iOS's dialog screens.
1260///
1261/// The `barrierDismissible` argument determines whether clicking outside the
1262/// popup results in dismissal. It is `true` by default.
1263///
1264/// The `useRootNavigator` argument is used to determine whether to push the
1265/// popup to the [Navigator] furthest from or nearest to the given `context`. It
1266/// is `true` by default.
1267///
1268/// The `semanticsDismissible` argument is used to determine whether the
1269/// semantics of the modal barrier are included in the semantics tree.
1270///
1271/// The `routeSettings` argument is used to provide [RouteSettings] to the
1272/// created Route.
1273///
1274/// The `builder` argument typically builds a [CupertinoActionSheet] widget.
1275/// Content below the widget is dimmed with a [ModalBarrier]. The widget built
1276/// by the `builder` does not share a context with the location that
1277/// [showCupertinoModalPopup] is originally called from. Use a
1278/// [StatefulBuilder] or a custom [StatefulWidget] if the widget needs to
1279/// update dynamically.
1280///
1281/// The [requestFocus] parameter is used to specify whether the popup should
1282/// request focus when shown.
1283/// {@macro flutter.widgets.navigator.Route.requestFocus}
1284///
1285/// {@macro flutter.widgets.RawDialogRoute}
1286///
1287/// Returns a `Future` that resolves to the value that was passed to
1288/// [Navigator.pop] when the popup was closed.
1289///
1290/// ### State Restoration in Modals
1291///
1292/// Using this method will not enable state restoration for the modal. In order
1293/// to enable state restoration for a modal, use [Navigator.restorablePush]
1294/// or [Navigator.restorablePushNamed] with [CupertinoModalPopupRoute].
1295///
1296/// For more information about state restoration, see [RestorationManager].
1297///
1298/// {@tool dartpad}
1299/// This sample demonstrates how to create a restorable Cupertino modal route.
1300/// This is accomplished by enabling state restoration by specifying
1301/// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to
1302/// push [CupertinoModalPopupRoute] when the [CupertinoButton] is tapped.
1303///
1304/// {@macro flutter.widgets.RestorationManager}
1305///
1306/// ** See code in examples/api/lib/cupertino/route/show_cupertino_modal_popup.0.dart **
1307/// {@end-tool}
1308///
1309/// See also:
1310///
1311/// * [DisplayFeatureSubScreen], which documents the specifics of how
1312/// [DisplayFeature]s can split the screen into sub-screens.
1313/// * [CupertinoActionSheet], which is the widget usually returned by the
1314/// `builder` argument to [showCupertinoModalPopup].
1315/// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
1316Future<T?> showCupertinoModalPopup<T>({
1317 required BuildContext context,
1318 required WidgetBuilder builder,
1319 ImageFilter? filter,
1320 Color barrierColor = kCupertinoModalBarrierColor,
1321 bool barrierDismissible = true,
1322 bool useRootNavigator = true,
1323 bool semanticsDismissible = false,
1324 RouteSettings? routeSettings,
1325 Offset? anchorPoint,
1326 bool? requestFocus,
1327}) {
1328 return Navigator.of(context, rootNavigator: useRootNavigator).push(
1329 CupertinoModalPopupRoute<T>(
1330 builder: builder,
1331 filter: filter,
1332 barrierColor: CupertinoDynamicColor.resolve(barrierColor, context),
1333 barrierDismissible: barrierDismissible,
1334 semanticsDismissible: semanticsDismissible,
1335 settings: routeSettings,
1336 anchorPoint: anchorPoint,
1337 requestFocus: requestFocus,
1338 ),
1339 );
1340}
1341
1342Widget _buildCupertinoDialogTransitions(
1343 BuildContext context,
1344 Animation<double> animation,
1345 Animation<double> secondaryAnimation,
1346 Widget child,
1347) {
1348 return child;
1349}
1350
1351/// Displays an iOS-style dialog above the current contents of the app, with
1352/// iOS-style entrance and exit animations, modal barrier color, and modal
1353/// barrier behavior (by default, the dialog is not dismissible with a tap on
1354/// the barrier).
1355///
1356/// This function takes a `builder` which typically builds a [CupertinoAlertDialog]
1357/// widget. Content below the dialog is dimmed with a [ModalBarrier]. The widget
1358/// returned by the `builder` does not share a context with the location that
1359/// [showCupertinoDialog] is originally called from. Use a [StatefulBuilder] or
1360/// a custom [StatefulWidget] if the dialog needs to update dynamically.
1361///
1362/// The `context` argument is used to look up the [Navigator] for the dialog.
1363/// It is only used when the method is called. Its corresponding widget can
1364/// be safely removed from the tree before the dialog is closed.
1365///
1366/// The `useRootNavigator` argument is used to determine whether to push the
1367/// dialog to the [Navigator] furthest from or nearest to the given `context`.
1368/// By default, `useRootNavigator` is `true` and the dialog route created by
1369/// this method is pushed to the root navigator.
1370///
1371/// {@macro flutter.material.dialog.requestFocus}
1372/// {@macro flutter.widgets.navigator.Route.requestFocus}
1373///
1374/// {@macro flutter.widgets.RawDialogRoute}
1375///
1376/// If the application has multiple [Navigator] objects, it may be necessary to
1377/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
1378/// dialog rather than just `Navigator.pop(context, result)`.
1379///
1380/// Returns a [Future] that resolves to the value (if any) that was passed to
1381/// [Navigator.pop] when the dialog was closed.
1382///
1383/// ### State Restoration in Dialogs
1384///
1385/// Using this method will not enable state restoration for the dialog. In order
1386/// to enable state restoration for a dialog, use [Navigator.restorablePush]
1387/// or [Navigator.restorablePushNamed] with [CupertinoDialogRoute].
1388///
1389/// For more information about state restoration, see [RestorationManager].
1390///
1391/// {@tool dartpad}
1392/// This sample demonstrates how to create a restorable Cupertino dialog. This is
1393/// accomplished by enabling state restoration by specifying
1394/// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to
1395/// push [CupertinoDialogRoute] when the [CupertinoButton] is tapped.
1396///
1397/// {@macro flutter.widgets.RestorationManager}
1398///
1399/// ** See code in examples/api/lib/cupertino/route/show_cupertino_dialog.0.dart **
1400/// {@end-tool}
1401///
1402/// See also:
1403///
1404/// * [CupertinoAlertDialog], an iOS-style alert dialog.
1405/// * [showDialog], which displays a Material-style dialog.
1406/// * [showGeneralDialog], which allows for customization of the dialog popup.
1407/// * [DisplayFeatureSubScreen], which documents the specifics of how
1408/// [DisplayFeature]s can split the screen into sub-screens.
1409/// * <https://developer.apple.com/design/human-interface-guidelines/alerts/>
1410Future<T?> showCupertinoDialog<T>({
1411 required BuildContext context,
1412 required WidgetBuilder builder,
1413 String? barrierLabel,
1414 Color? barrierColor,
1415 bool useRootNavigator = true,
1416 bool barrierDismissible = false,
1417 RouteSettings? routeSettings,
1418 Offset? anchorPoint,
1419 bool? requestFocus,
1420}) {
1421 return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(
1422 CupertinoDialogRoute<T>(
1423 builder: builder,
1424 context: context,
1425 barrierDismissible: barrierDismissible,
1426 barrierLabel: barrierLabel,
1427 barrierColor: barrierColor,
1428 settings: routeSettings,
1429 anchorPoint: anchorPoint,
1430 requestFocus: requestFocus,
1431 ),
1432 );
1433}
1434
1435/// A dialog route that shows an iOS-style dialog.
1436///
1437/// It is used internally by [showCupertinoDialog] or can be directly pushed
1438/// onto the [Navigator] stack to enable state restoration. See
1439/// [showCupertinoDialog] for a state restoration app example.
1440///
1441/// This function takes a `builder` which typically builds a [Dialog] widget.
1442/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
1443/// returned by the `builder` does not share a context with the location that
1444/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
1445/// custom [StatefulWidget] if the dialog needs to update dynamically.
1446///
1447/// The `context` argument is used to look up
1448/// [CupertinoLocalizations.modalBarrierDismissLabel], which provides the
1449/// modal with a localized accessibility label that will be used for the
1450/// modal's barrier. However, a custom `barrierLabel` can be passed in as well.
1451///
1452/// The `barrierDismissible` argument is used to indicate whether tapping on the
1453/// barrier will dismiss the dialog. It is `true` by default and cannot be `null`.
1454///
1455/// The `barrierColor` argument is used to specify the color of the modal
1456/// barrier that darkens everything below the dialog. If `null`, then
1457/// [CupertinoDynamicColor.resolve] is used to compute the modal color.
1458///
1459/// The `settings` argument define the settings for this route. See
1460/// [RouteSettings] for details.
1461///
1462/// {@macro flutter.widgets.RawDialogRoute}
1463///
1464/// See also:
1465///
1466/// * [showCupertinoDialog], which is a way to display
1467/// an iOS-style dialog.
1468/// * [showGeneralDialog], which allows for customization of the dialog popup.
1469/// * [showDialog], which displays a Material dialog.
1470/// * [DisplayFeatureSubScreen], which documents the specifics of how
1471/// [DisplayFeature]s can split the screen into sub-screens.
1472class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
1473 /// A dialog route that shows an iOS-style dialog.
1474 CupertinoDialogRoute({
1475 required WidgetBuilder builder,
1476 required BuildContext context,
1477 super.barrierDismissible,
1478 Color? barrierColor,
1479 String? barrierLabel,
1480 // This transition duration was eyeballed comparing with iOS
1481 super.transitionDuration = const Duration(milliseconds: 250),
1482 this.transitionBuilder,
1483 super.settings,
1484 super.requestFocus,
1485 super.anchorPoint,
1486 }) : super(
1487 pageBuilder:
1488 (
1489 BuildContext context,
1490 Animation<double> animation,
1491 Animation<double> secondaryAnimation,
1492 ) {
1493 return builder(context);
1494 },
1495 transitionBuilder: transitionBuilder ?? _buildCupertinoDialogTransitions,
1496 barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel,
1497 barrierColor:
1498 barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
1499 );
1500
1501 /// Custom transition builder
1502 RouteTransitionsBuilder? transitionBuilder;
1503
1504 CurvedAnimation? _fadeAnimation;
1505
1506 @override
1507 Simulation createSimulation({required bool forward}) {
1508 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
1509 final double end = forward ? 1.0 : 0.0;
1510 return SpringSimulation(
1511 _kStandardSpring,
1512 controller!.value,
1513 end,
1514 0,
1515 tolerance: _kStandardTolerance,
1516 snapToEnd: true,
1517 );
1518 }
1519
1520 @override
1521 Widget buildTransitions(
1522 BuildContext context,
1523 Animation<double> animation,
1524 Animation<double> secondaryAnimation,
1525 Widget child,
1526 ) {
1527 if (transitionBuilder != null) {
1528 return super.buildTransitions(context, animation, secondaryAnimation, child);
1529 }
1530
1531 if (animation.status == AnimationStatus.reverse) {
1532 return FadeTransition(opacity: animation, child: child);
1533 }
1534 return FadeTransition(
1535 opacity: animation,
1536 child: ScaleTransition(scale: animation.drive(_dialogScaleTween), child: child),
1537 );
1538 }
1539
1540 @override
1541 void dispose() {
1542 _fadeAnimation?.dispose();
1543 super.dispose();
1544 }
1545
1546 // The curve and initial scale values were mostly eyeballed from iOS, however
1547 // they reuse the same animation curve that was modeled after native page
1548 // transitions.
1549 static final Tween<double> _dialogScaleTween = Tween<double>(begin: 1.3, end: 1.0);
1550}
1551