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 =
142 previousRoute is CupertinoRouteTransitionMixin ? previousRoute.title : null;
143 if (_previousTitle == null) {
144 _previousTitle = ValueNotifier<String?>(previousTitleString);
145 } else {
146 _previousTitle!.value = previousTitleString;
147 }
148 super.didChangePrevious(previousRoute);
149 }
150
151 /// The duration of the page transition.
152 ///
153 /// A relatively rigorous eyeball estimation.
154 static const Duration kTransitionDuration = Duration(milliseconds: 500);
155
156 @override
157 Duration get transitionDuration => kTransitionDuration;
158
159 @override
160 Color? get barrierColor => fullscreenDialog ? null : _kCupertinoPageTransitionBarrierColor;
161
162 @override
163 String? get barrierLabel => null;
164
165 @override
166 bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
167 // Don't perform outgoing animation if the next route is a fullscreen dialog.
168 final bool nextRouteIsNotFullscreen =
169 (nextRoute is! PageRoute<T>) || !nextRoute.fullscreenDialog;
170
171 // If the next route has a delegated transition, then this route is able to
172 // use that delegated transition to smoothly sync with the next route's
173 // transition.
174 final bool nextRouteHasDelegatedTransition =
175 nextRoute is ModalRoute<T> && nextRoute.delegatedTransition != null;
176
177 // Otherwise if the next route has the same route transition mixin as this
178 // one, then this route will already be synced with its transition.
179 return nextRouteIsNotFullscreen &&
180 ((nextRoute is CupertinoRouteTransitionMixin) || nextRouteHasDelegatedTransition);
181 }
182
183 @override
184 bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) {
185 // Supress previous route from transitioning if this is a fullscreenDialog route.
186 return previousRoute is PageRoute && !fullscreenDialog;
187 }
188
189 @override
190 Widget buildPage(
191 BuildContext context,
192 Animation<double> animation,
193 Animation<double> secondaryAnimation,
194 ) {
195 final Widget child = buildContent(context);
196 return Semantics(scopesRoute: true, explicitChildNodes: true, child: child);
197 }
198
199 // Called by _CupertinoBackGestureDetector when a pop ("back") drag start
200 // gesture is detected. The returned controller handles all of the subsequent
201 // drag events.
202 static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) {
203 assert(route.popGestureEnabled);
204
205 return _CupertinoBackGestureController<T>(
206 navigator: route.navigator!,
207 getIsCurrent: () => route.isCurrent,
208 getIsActive: () => route.isActive,
209 controller: route.controller!, // protected access
210 );
211 }
212
213 /// Returns a [CupertinoFullscreenDialogTransition] if [route] is a full
214 /// screen dialog, otherwise a [CupertinoPageTransition] is returned.
215 ///
216 /// Used by [CupertinoPageRoute.buildTransitions].
217 ///
218 /// This method can be applied to any [PageRoute], not just
219 /// [CupertinoPageRoute]. It's typically used to provide a Cupertino style
220 /// horizontal transition for material widgets when the target platform
221 /// is [TargetPlatform.iOS].
222 ///
223 /// See also:
224 ///
225 /// * [CupertinoPageTransitionsBuilder], which uses this method to define a
226 /// [PageTransitionsBuilder] for the [PageTransitionsTheme].
227 static Widget buildPageTransitions<T>(
228 PageRoute<T> route,
229 BuildContext context,
230 Animation<double> animation,
231 Animation<double> secondaryAnimation,
232 Widget child,
233 ) {
234 // Check if the route has an animation that's currently participating
235 // in a back swipe gesture.
236 //
237 // In the middle of a back gesture drag, let the transition be linear to
238 // match finger motions.
239 final bool linearTransition = route.popGestureInProgress;
240 if (route.fullscreenDialog) {
241 return CupertinoFullscreenDialogTransition(
242 primaryRouteAnimation: animation,
243 secondaryRouteAnimation: secondaryAnimation,
244 linearTransition: linearTransition,
245 child: child,
246 );
247 } else {
248 return CupertinoPageTransition(
249 primaryRouteAnimation: animation,
250 secondaryRouteAnimation: secondaryAnimation,
251 linearTransition: linearTransition,
252 child: _CupertinoBackGestureDetector<T>(
253 enabledCallback: () => route.popGestureEnabled,
254 onStartPopGesture: () => _startPopGesture<T>(route),
255 child: child,
256 ),
257 );
258 }
259 }
260
261 @override
262 Widget buildTransitions(
263 BuildContext context,
264 Animation<double> animation,
265 Animation<double> secondaryAnimation,
266 Widget child,
267 ) {
268 return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child);
269 }
270}
271
272/// A modal route that replaces the entire screen with an iOS transition.
273///
274/// {@macro flutter.cupertino.cupertinoRouteTransitionMixin}
275///
276/// By default, when a modal route is replaced by another, the previous route
277/// remains in memory. To free all the resources when this is not necessary, set
278/// [maintainState] to false.
279///
280/// The type `T` specifies the return type of the route which can be supplied as
281/// the route is popped from the stack via [Navigator.pop] when an optional
282/// `result` can be provided.
283///
284/// If `barrierDismissible` is true, then pressing the escape key on the keyboard
285/// will cause the current route to be popped with null as the value.
286///
287/// See also:
288///
289/// * [CupertinoRouteTransitionMixin], for a mixin that provides iOS transition
290/// for this modal route.
291/// * [MaterialPageRoute], for an adaptive [PageRoute] that uses a
292/// platform-appropriate transition.
293/// * [CupertinoPageScaffold], for applications that have one page with a fixed
294/// navigation bar on top.
295/// * [CupertinoTabScaffold], for applications that have a tab bar at the
296/// bottom with multiple pages.
297/// * [CupertinoPage], for a [Page] version of this class.
298class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> {
299 /// Creates a page route for use in an iOS designed app.
300 ///
301 /// The [builder], [maintainState], and [fullscreenDialog] arguments must not
302 /// be null.
303 CupertinoPageRoute({
304 required this.builder,
305 this.title,
306 super.settings,
307 super.requestFocus,
308 this.maintainState = true,
309 super.fullscreenDialog,
310 super.allowSnapshotting = true,
311 super.barrierDismissible = false,
312 }) {
313 assert(opaque);
314 }
315
316 @override
317 DelegatedTransitionBuilder? get delegatedTransition =>
318 CupertinoPageTransition.delegatedTransition;
319
320 /// Builds the primary contents of the route.
321 final WidgetBuilder builder;
322
323 @override
324 Widget buildContent(BuildContext context) => builder(context);
325
326 @override
327 final String? title;
328
329 @override
330 final bool maintainState;
331
332 @override
333 String get debugLabel => '${super.debugLabel}(${settings.name})';
334}
335
336// A page-based version of CupertinoPageRoute.
337//
338// This route uses the builder from the page to build its content. This ensures
339// the content is up to date after page updates.
340class _PageBasedCupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> {
341 _PageBasedCupertinoPageRoute({required CupertinoPage<T> page, super.allowSnapshotting = true})
342 : super(settings: page) {
343 assert(opaque);
344 }
345
346 @override
347 DelegatedTransitionBuilder? get delegatedTransition =>
348 this.fullscreenDialog ? null : CupertinoPageTransition.delegatedTransition;
349
350 CupertinoPage<T> get _page => settings as CupertinoPage<T>;
351
352 @override
353 Widget buildContent(BuildContext context) => _page.child;
354
355 @override
356 String? get title => _page.title;
357
358 @override
359 bool get maintainState => _page.maintainState;
360
361 @override
362 bool get fullscreenDialog => _page.fullscreenDialog;
363
364 @override
365 String get debugLabel => '${super.debugLabel}(${_page.name})';
366}
367
368/// A page that creates a cupertino style [PageRoute].
369///
370/// {@macro flutter.cupertino.cupertinoRouteTransitionMixin}
371///
372/// By default, when a created modal route is replaced by another, the previous
373/// route remains in memory. To free all the resources when this is not
374/// necessary, set [maintainState] to false.
375///
376/// The type `T` specifies the return type of the route which can be supplied as
377/// the route is popped from the stack via [Navigator.transitionDelegate] by
378/// providing the optional `result` argument to the
379/// [RouteTransitionRecord.markForPop] in the [TransitionDelegate.resolve].
380///
381/// See also:
382///
383/// * [CupertinoPageRoute], for a [PageRoute] version of this class.
384class CupertinoPage<T> extends Page<T> {
385 /// Creates a cupertino page.
386 const CupertinoPage({
387 required this.child,
388 this.maintainState = true,
389 this.title,
390 this.fullscreenDialog = false,
391 this.allowSnapshotting = true,
392 super.canPop,
393 super.onPopInvoked,
394 super.key,
395 super.name,
396 super.arguments,
397 super.restorationId,
398 });
399
400 /// The content to be shown in the [Route] created by this page.
401 final Widget child;
402
403 /// {@macro flutter.cupertino.CupertinoRouteTransitionMixin.title}
404 final String? title;
405
406 /// {@macro flutter.widgets.ModalRoute.maintainState}
407 final bool maintainState;
408
409 /// {@macro flutter.widgets.PageRoute.fullscreenDialog}
410 final bool fullscreenDialog;
411
412 /// {@macro flutter.widgets.TransitionRoute.allowSnapshotting}
413 final bool allowSnapshotting;
414
415 @override
416 Route<T> createRoute(BuildContext context) {
417 return _PageBasedCupertinoPageRoute<T>(page: this, allowSnapshotting: allowSnapshotting);
418 }
419}
420
421/// Provides an iOS-style page transition animation.
422///
423/// The page slides in from the right and exits in reverse. It also shifts to the left in
424/// a parallax motion when another page enters to cover it.
425class CupertinoPageTransition extends StatefulWidget {
426 /// Creates an iOS-style page transition.
427 ///
428 const CupertinoPageTransition({
429 super.key,
430 required this.primaryRouteAnimation,
431 required this.secondaryRouteAnimation,
432 required this.child,
433 required this.linearTransition,
434 });
435
436 /// The widget below this widget in the tree.
437 final Widget child;
438
439 /// * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0
440 /// when this screen is being pushed.
441 final Animation<double> primaryRouteAnimation;
442
443 /// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0
444 /// when another screen is being pushed on top of this one.
445 final Animation<double> secondaryRouteAnimation;
446
447 /// * `linearTransition` is whether to perform the transitions linearly.
448 /// Used to precisely track back gesture drags.
449 final bool linearTransition;
450
451 /// The Cupertino styled [DelegatedTransitionBuilder] provided to the previous
452 /// route.
453 ///
454 /// {@macro flutter.widgets.delegatedTransition}
455 static Widget? delegatedTransition(
456 BuildContext context,
457 Animation<double> animation,
458 Animation<double> secondaryAnimation,
459 bool allowSnapshotting,
460 Widget? child,
461 ) {
462 final CurvedAnimation animation = CurvedAnimation(
463 parent: secondaryAnimation,
464 curve: Curves.linearToEaseOut,
465 reverseCurve: Curves.easeInToLinear,
466 );
467 final Animation<Offset> delegatedPositionAnimation = animation.drive(_kMiddleLeftTween);
468 animation.dispose();
469
470 assert(debugCheckHasDirectionality(context));
471 final TextDirection textDirection = Directionality.of(context);
472 return SlideTransition(
473 position: delegatedPositionAnimation,
474 textDirection: textDirection,
475 transformHitTests: false,
476 child: child,
477 );
478 }
479
480 @override
481 State<CupertinoPageTransition> createState() => _CupertinoPageTransitionState();
482}
483
484class _CupertinoPageTransitionState extends State<CupertinoPageTransition> {
485 // When this page is coming in to cover another page.
486 late Animation<Offset> _primaryPositionAnimation;
487 // When this page is becoming covered by another page.
488 late Animation<Offset> _secondaryPositionAnimation;
489 // Shadow of page which is coming in to cover another page.
490 late Animation<Decoration> _primaryShadowAnimation;
491 // Curve of primary page which is coming in to cover another page.
492 CurvedAnimation? _primaryPositionCurve;
493 // Curve of secondary page which is becoming covered by another page.
494 CurvedAnimation? _secondaryPositionCurve;
495 // Curve of primary page's shadow.
496 CurvedAnimation? _primaryShadowCurve;
497
498 @override
499 void initState() {
500 super.initState();
501 _setupAnimation();
502 }
503
504 @override
505 void didUpdateWidget(covariant CupertinoPageTransition oldWidget) {
506 super.didUpdateWidget(oldWidget);
507 if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation ||
508 oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation ||
509 oldWidget.linearTransition != widget.linearTransition) {
510 _disposeCurve();
511 _setupAnimation();
512 }
513 }
514
515 @override
516 void dispose() {
517 _disposeCurve();
518 super.dispose();
519 }
520
521 void _disposeCurve() {
522 _primaryPositionCurve?.dispose();
523 _secondaryPositionCurve?.dispose();
524 _primaryShadowCurve?.dispose();
525 _primaryPositionCurve = null;
526 _secondaryPositionCurve = null;
527 _primaryShadowCurve = null;
528 }
529
530 void _setupAnimation() {
531 if (!widget.linearTransition) {
532 _primaryPositionCurve = CurvedAnimation(
533 parent: widget.primaryRouteAnimation,
534 curve: Curves.fastEaseInToSlowEaseOut,
535 reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped,
536 );
537 _secondaryPositionCurve = CurvedAnimation(
538 parent: widget.secondaryRouteAnimation,
539 curve: Curves.linearToEaseOut,
540 reverseCurve: Curves.easeInToLinear,
541 );
542 _primaryShadowCurve = CurvedAnimation(
543 parent: widget.primaryRouteAnimation,
544 curve: Curves.linearToEaseOut,
545 );
546 }
547 _primaryPositionAnimation = (_primaryPositionCurve ?? widget.primaryRouteAnimation).drive(
548 _kRightMiddleTween,
549 );
550 _secondaryPositionAnimation = (_secondaryPositionCurve ?? widget.secondaryRouteAnimation).drive(
551 _kMiddleLeftTween,
552 );
553 _primaryShadowAnimation = (_primaryShadowCurve ?? widget.primaryRouteAnimation).drive(
554 _CupertinoEdgeShadowDecoration.kTween,
555 );
556 }
557
558 @override
559 Widget build(BuildContext context) {
560 assert(debugCheckHasDirectionality(context));
561 final TextDirection textDirection = Directionality.of(context);
562 return SlideTransition(
563 position: _secondaryPositionAnimation,
564 textDirection: textDirection,
565 transformHitTests: false,
566 child: SlideTransition(
567 position: _primaryPositionAnimation,
568 textDirection: textDirection,
569 child: DecoratedBoxTransition(decoration: _primaryShadowAnimation, child: widget.child),
570 ),
571 );
572 }
573}
574
575/// An iOS-style transition used for summoning fullscreen dialogs.
576///
577/// For example, used when creating a new calendar event by bringing in the next
578/// screen from the bottom.
579class CupertinoFullscreenDialogTransition extends StatefulWidget {
580 /// Creates an iOS-style transition used for summoning fullscreen dialogs.
581 ///
582 const CupertinoFullscreenDialogTransition({
583 super.key,
584 required this.primaryRouteAnimation,
585 required this.secondaryRouteAnimation,
586 required this.child,
587 required this.linearTransition,
588 });
589
590 /// * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0
591 /// when this screen is being pushed.
592 final Animation<double> primaryRouteAnimation;
593
594 /// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0
595 /// when another screen is being pushed on top of this one.
596 final Animation<double> secondaryRouteAnimation;
597
598 /// * `linearTransition` is whether to perform the transitions linearly.
599 /// Used to precisely track back gesture drags.
600 final bool linearTransition;
601
602 /// The widget below this widget in the tree.
603 final Widget child;
604
605 @override
606 State<CupertinoFullscreenDialogTransition> createState() =>
607 _CupertinoFullscreenDialogTransitionState();
608}
609
610class _CupertinoFullscreenDialogTransitionState extends State<CupertinoFullscreenDialogTransition> {
611 /// When this page is coming in to cover another page.
612 late Animation<Offset> _primaryPositionAnimation;
613
614 /// When this page is becoming covered by another page.
615 late Animation<Offset> _secondaryPositionAnimation;
616
617 /// Curve of primary page which is coming in to cover another page.
618 CurvedAnimation? _primaryPositionCurve;
619
620 /// Curve of secondary page which is becoming covered by another page.
621 CurvedAnimation? _secondaryPositionCurve;
622
623 @override
624 void initState() {
625 super.initState();
626 _setupAnimation();
627 }
628
629 @override
630 void didUpdateWidget(covariant CupertinoFullscreenDialogTransition oldWidget) {
631 super.didUpdateWidget(oldWidget);
632 if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation ||
633 oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation ||
634 oldWidget.linearTransition != widget.linearTransition) {
635 _disposeCurve();
636 _setupAnimation();
637 }
638 }
639
640 @override
641 void dispose() {
642 _disposeCurve();
643 super.dispose();
644 }
645
646 void _disposeCurve() {
647 _primaryPositionCurve?.dispose();
648 _secondaryPositionCurve?.dispose();
649 _primaryPositionCurve = null;
650 _secondaryPositionCurve = null;
651 }
652
653 void _setupAnimation() {
654 _primaryPositionAnimation = (_primaryPositionCurve = CurvedAnimation(
655 parent: widget.primaryRouteAnimation,
656 curve: Curves.linearToEaseOut,
657 // The curve must be flipped so that the reverse animation doesn't play
658 // an ease-in curve, which iOS does not use.
659 reverseCurve: Curves.linearToEaseOut.flipped,
660 ))
661 .drive(_kBottomUpTween);
662 _secondaryPositionAnimation = (widget.linearTransition
663 ? widget.secondaryRouteAnimation
664 : _secondaryPositionCurve = CurvedAnimation(
665 parent: widget.secondaryRouteAnimation,
666 curve: Curves.linearToEaseOut,
667 reverseCurve: Curves.easeInToLinear,
668 ))
669 .drive(_kMiddleLeftTween);
670 }
671
672 @override
673 Widget build(BuildContext context) {
674 assert(debugCheckHasDirectionality(context));
675 final TextDirection textDirection = Directionality.of(context);
676 return SlideTransition(
677 position: _secondaryPositionAnimation,
678 textDirection: textDirection,
679 transformHitTests: false,
680 child: SlideTransition(position: _primaryPositionAnimation, child: widget.child),
681 );
682 }
683}
684
685/// This is the widget side of [_CupertinoBackGestureController].
686///
687/// This widget provides a gesture recognizer which, when it determines the
688/// route can be closed with a back gesture, creates the controller and
689/// feeds it the input from the gesture recognizer.
690///
691/// The gesture data is converted from absolute coordinates to logical
692/// coordinates by this widget.
693///
694/// The type `T` specifies the return type of the route with which this gesture
695/// detector is associated.
696class _CupertinoBackGestureDetector<T> extends StatefulWidget {
697 const _CupertinoBackGestureDetector({
698 super.key,
699 required this.enabledCallback,
700 required this.onStartPopGesture,
701 required this.child,
702 });
703
704 final Widget child;
705
706 final ValueGetter<bool> enabledCallback;
707
708 final ValueGetter<_CupertinoBackGestureController<T>> onStartPopGesture;
709
710 @override
711 _CupertinoBackGestureDetectorState<T> createState() => _CupertinoBackGestureDetectorState<T>();
712}
713
714class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureDetector<T>> {
715 _CupertinoBackGestureController<T>? _backGestureController;
716
717 late HorizontalDragGestureRecognizer _recognizer;
718
719 @override
720 void initState() {
721 super.initState();
722 _recognizer =
723 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 =
1083 Paint()
1084 ..color =
1085 Color.lerp(
1086 colors[bandColorIndex],
1087 colors[bandColorIndex + 1],
1088 (dx % bandWidth) / bandWidth,
1089 )!;
1090 final double x = start + shadowDirection * dx;
1091 canvas.drawRect(Rect.fromLTWH(x - 1.0, offset.dy, 1.0, shadowHeight), paint);
1092 }
1093 }
1094}
1095
1096// The stiffness used by dialogs and action sheets.
1097//
1098// The stiffness value is obtained by examining the properties of
1099// `CASpringAnimation` in Xcode. The damping value is derived similarly, with
1100// additional precision calculated based on `_kStandardStiffness` to ensure a
1101// damping ratio of 1 (critically damped): damping = 2 * sqrt(stiffness)
1102const double _kStandardStiffness = 522.35;
1103const double _kStandardDamping = 45.7099552;
1104const SpringDescription _kStandardSpring = SpringDescription(
1105 mass: 1,
1106 stiffness: _kStandardStiffness,
1107 damping: _kStandardDamping,
1108);
1109// The iOS spring animation duration is 0.404 seconds, based on the properties
1110// of `CASpringAnimation` in Xcode. At this point, the spring's position
1111// `x(0.404)` is approximately 0.9990000, suggesting that iOS uses a position
1112// tolerance of 1e-3 (matching the default `_epsilonDefault` value).
1113//
1114// However, the spring's velocity `dx(0.404)` is about 0.02, indicating that iOS
1115// may not consider velocity when determining the animation's end condition. To
1116// account for this, a larger velocity tolerance is applied here for added
1117// safety.
1118const Tolerance _kStandardTolerance = Tolerance(velocity: 0.03);
1119
1120/// A route that shows a modal iOS-style popup that slides up from the
1121/// bottom of the screen.
1122///
1123/// Such a popup is an alternative to a menu or a dialog and prevents the user
1124/// from interacting with the rest of the app.
1125///
1126/// It is used internally by [showCupertinoModalPopup] or can be directly pushed
1127/// onto the [Navigator] stack to enable state restoration. See
1128/// [showCupertinoModalPopup] for a state restoration app example.
1129///
1130/// The `barrierColor` argument determines the [Color] of the barrier underneath
1131/// the popup. When unspecified, the barrier color defaults to a light opacity
1132/// black scrim based on iOS's dialog screens. To correctly have iOS resolve
1133/// to the appropriate modal colors, pass in
1134/// `CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context)`.
1135///
1136/// The `barrierDismissible` argument determines whether clicking outside the
1137/// popup results in dismissal. It is `true` by default.
1138///
1139/// The `semanticsDismissible` argument is used to determine whether the
1140/// semantics of the modal barrier are included in the semantics tree.
1141///
1142/// The `routeSettings` argument is used to provide [RouteSettings] to the
1143/// created Route.
1144///
1145/// {@macro flutter.widgets.RawDialogRoute}
1146///
1147/// See also:
1148///
1149/// * [DisplayFeatureSubScreen], which documents the specifics of how
1150/// [DisplayFeature]s can split the screen into sub-screens.
1151/// * [CupertinoActionSheet], which is the widget usually returned by the
1152/// `builder` argument.
1153/// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
1154class CupertinoModalPopupRoute<T> extends PopupRoute<T> {
1155 /// A route that shows a modal iOS-style popup that slides up from the
1156 /// bottom of the screen.
1157 CupertinoModalPopupRoute({
1158 required this.builder,
1159 this.barrierLabel = 'Dismiss',
1160 this.barrierColor = kCupertinoModalBarrierColor,
1161 bool barrierDismissible = true,
1162 bool semanticsDismissible = false,
1163 super.filter,
1164 super.settings,
1165 super.requestFocus,
1166 this.anchorPoint,
1167 }) : _barrierDismissible = barrierDismissible,
1168 _semanticsDismissible = semanticsDismissible;
1169
1170 /// A builder that builds the widget tree for the [CupertinoModalPopupRoute].
1171 ///
1172 /// The [builder] argument typically builds a [CupertinoActionSheet] widget.
1173 ///
1174 /// Content below the widget is dimmed with a [ModalBarrier]. The widget built
1175 /// by the [builder] does not share a context with the route it was originally
1176 /// built from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the
1177 /// widget needs to update dynamically.
1178 final WidgetBuilder builder;
1179
1180 final bool _barrierDismissible;
1181
1182 final bool _semanticsDismissible;
1183
1184 @override
1185 final String barrierLabel;
1186
1187 @override
1188 final Color? barrierColor;
1189
1190 @override
1191 bool get barrierDismissible => _barrierDismissible;
1192
1193 @override
1194 bool get semanticsDismissible => _semanticsDismissible;
1195
1196 @override
1197 Duration get transitionDuration => _kModalPopupTransitionDuration;
1198
1199 /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
1200 final Offset? anchorPoint;
1201
1202 @override
1203 Simulation createSimulation({required bool forward}) {
1204 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
1205 final double end = forward ? 1.0 : 0.0;
1206 return SpringSimulation(
1207 _kStandardSpring,
1208 controller!.value,
1209 end,
1210 0,
1211 tolerance: _kStandardTolerance,
1212 snapToEnd: true,
1213 );
1214 }
1215
1216 @override
1217 Widget buildPage(
1218 BuildContext context,
1219 Animation<double> animation,
1220 Animation<double> secondaryAnimation,
1221 ) {
1222 return CupertinoUserInterfaceLevel(
1223 data: CupertinoUserInterfaceLevelData.elevated,
1224 child: DisplayFeatureSubScreen(anchorPoint: anchorPoint, child: Builder(builder: builder)),
1225 );
1226 }
1227
1228 @override
1229 Widget buildTransitions(
1230 BuildContext context,
1231 Animation<double> animation,
1232 Animation<double> secondaryAnimation,
1233 Widget child,
1234 ) {
1235 return Align(
1236 alignment: Alignment.bottomCenter,
1237 child: FractionalTranslation(translation: _offsetTween.evaluate(animation), child: child),
1238 );
1239 }
1240
1241 static final Tween<Offset> _offsetTween = Tween<Offset>(
1242 begin: const Offset(0.0, 1.0),
1243 end: Offset.zero,
1244 );
1245}
1246
1247/// Shows a modal iOS-style popup that slides up from the bottom of the screen.
1248///
1249/// Such a popup is an alternative to a menu or a dialog and prevents the user
1250/// from interacting with the rest of the app.
1251///
1252/// The `context` argument is used to look up the [Navigator] for the popup.
1253/// It is only used when the method is called. Its corresponding widget can be
1254/// safely removed from the tree before the popup is closed.
1255///
1256/// The `barrierColor` argument determines the [Color] of the barrier underneath
1257/// the popup. When unspecified, the barrier color defaults to a light opacity
1258/// black scrim based on iOS's dialog screens.
1259///
1260/// The `barrierDismissible` argument determines whether clicking outside the
1261/// popup results in dismissal. It is `true` by default.
1262///
1263/// The `useRootNavigator` argument is used to determine whether to push the
1264/// popup to the [Navigator] furthest from or nearest to the given `context`. It
1265/// is `true` by default.
1266///
1267/// The `semanticsDismissible` argument is used to determine whether the
1268/// semantics of the modal barrier are included in the semantics tree.
1269///
1270/// The `routeSettings` argument is used to provide [RouteSettings] to the
1271/// created Route.
1272///
1273/// The `builder` argument typically builds a [CupertinoActionSheet] widget.
1274/// Content below the widget is dimmed with a [ModalBarrier]. The widget built
1275/// by the `builder` does not share a context with the location that
1276/// [showCupertinoModalPopup] is originally called from. Use a
1277/// [StatefulBuilder] or a custom [StatefulWidget] if the widget needs to
1278/// update dynamically.
1279///
1280/// The [requestFocus] parameter is used to specify whether the popup should
1281/// request focus when shown.
1282/// {@macro flutter.widgets.navigator.Route.requestFocus}
1283///
1284/// {@macro flutter.widgets.RawDialogRoute}
1285///
1286/// Returns a `Future` that resolves to the value that was passed to
1287/// [Navigator.pop] when the popup was closed.
1288///
1289/// ### State Restoration in Modals
1290///
1291/// Using this method will not enable state restoration for the modal. In order
1292/// to enable state restoration for a modal, use [Navigator.restorablePush]
1293/// or [Navigator.restorablePushNamed] with [CupertinoModalPopupRoute].
1294///
1295/// For more information about state restoration, see [RestorationManager].
1296///
1297/// {@tool dartpad}
1298/// This sample demonstrates how to create a restorable Cupertino modal route.
1299/// This is accomplished by enabling state restoration by specifying
1300/// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to
1301/// push [CupertinoModalPopupRoute] when the [CupertinoButton] is tapped.
1302///
1303/// {@macro flutter.widgets.RestorationManager}
1304///
1305/// ** See code in examples/api/lib/cupertino/route/show_cupertino_modal_popup.0.dart **
1306/// {@end-tool}
1307///
1308/// See also:
1309///
1310/// * [DisplayFeatureSubScreen], which documents the specifics of how
1311/// [DisplayFeature]s can split the screen into sub-screens.
1312/// * [CupertinoActionSheet], which is the widget usually returned by the
1313/// `builder` argument to [showCupertinoModalPopup].
1314/// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
1315Future<T?> showCupertinoModalPopup<T>({
1316 required BuildContext context,
1317 required WidgetBuilder builder,
1318 ImageFilter? filter,
1319 Color barrierColor = kCupertinoModalBarrierColor,
1320 bool barrierDismissible = true,
1321 bool useRootNavigator = true,
1322 bool semanticsDismissible = false,
1323 RouteSettings? routeSettings,
1324 Offset? anchorPoint,
1325 bool? requestFocus,
1326}) {
1327 return Navigator.of(context, rootNavigator: useRootNavigator).push(
1328 CupertinoModalPopupRoute<T>(
1329 builder: builder,
1330 filter: filter,
1331 barrierColor: CupertinoDynamicColor.resolve(barrierColor, context),
1332 barrierDismissible: barrierDismissible,
1333 semanticsDismissible: semanticsDismissible,
1334 settings: routeSettings,
1335 anchorPoint: anchorPoint,
1336 requestFocus: requestFocus,
1337 ),
1338 );
1339}
1340
1341Widget _buildCupertinoDialogTransitions(
1342 BuildContext context,
1343 Animation<double> animation,
1344 Animation<double> secondaryAnimation,
1345 Widget child,
1346) {
1347 return child;
1348}
1349
1350/// Displays an iOS-style dialog above the current contents of the app, with
1351/// iOS-style entrance and exit animations, modal barrier color, and modal
1352/// barrier behavior (by default, the dialog is not dismissible with a tap on
1353/// the barrier).
1354///
1355/// This function takes a `builder` which typically builds a [CupertinoAlertDialog]
1356/// widget. Content below the dialog is dimmed with a [ModalBarrier]. The widget
1357/// returned by the `builder` does not share a context with the location that
1358/// [showCupertinoDialog] is originally called from. Use a [StatefulBuilder] or
1359/// a custom [StatefulWidget] if the dialog needs to update dynamically.
1360///
1361/// The `context` argument is used to look up the [Navigator] for the dialog.
1362/// It is only used when the method is called. Its corresponding widget can
1363/// be safely removed from the tree before the dialog is closed.
1364///
1365/// The `useRootNavigator` argument is used to determine whether to push the
1366/// dialog to the [Navigator] furthest from or nearest to the given `context`.
1367/// By default, `useRootNavigator` is `true` and the dialog route created by
1368/// this method is pushed to the root navigator.
1369///
1370/// {@macro flutter.material.dialog.requestFocus}
1371/// {@macro flutter.widgets.navigator.Route.requestFocus}
1372///
1373/// {@macro flutter.widgets.RawDialogRoute}
1374///
1375/// If the application has multiple [Navigator] objects, it may be necessary to
1376/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
1377/// dialog rather than just `Navigator.pop(context, result)`.
1378///
1379/// Returns a [Future] that resolves to the value (if any) that was passed to
1380/// [Navigator.pop] when the dialog was closed.
1381///
1382/// ### State Restoration in Dialogs
1383///
1384/// Using this method will not enable state restoration for the dialog. In order
1385/// to enable state restoration for a dialog, use [Navigator.restorablePush]
1386/// or [Navigator.restorablePushNamed] with [CupertinoDialogRoute].
1387///
1388/// For more information about state restoration, see [RestorationManager].
1389///
1390/// {@tool dartpad}
1391/// This sample demonstrates how to create a restorable Cupertino dialog. This is
1392/// accomplished by enabling state restoration by specifying
1393/// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to
1394/// push [CupertinoDialogRoute] when the [CupertinoButton] is tapped.
1395///
1396/// {@macro flutter.widgets.RestorationManager}
1397///
1398/// ** See code in examples/api/lib/cupertino/route/show_cupertino_dialog.0.dart **
1399/// {@end-tool}
1400///
1401/// See also:
1402///
1403/// * [CupertinoAlertDialog], an iOS-style alert dialog.
1404/// * [showDialog], which displays a Material-style dialog.
1405/// * [showGeneralDialog], which allows for customization of the dialog popup.
1406/// * [DisplayFeatureSubScreen], which documents the specifics of how
1407/// [DisplayFeature]s can split the screen into sub-screens.
1408/// * <https://developer.apple.com/design/human-interface-guidelines/alerts/>
1409Future<T?> showCupertinoDialog<T>({
1410 required BuildContext context,
1411 required WidgetBuilder builder,
1412 String? barrierLabel,
1413 bool useRootNavigator = true,
1414 bool barrierDismissible = false,
1415 RouteSettings? routeSettings,
1416 Offset? anchorPoint,
1417 bool? requestFocus,
1418}) {
1419 return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(
1420 CupertinoDialogRoute<T>(
1421 builder: builder,
1422 context: context,
1423 barrierDismissible: barrierDismissible,
1424 barrierLabel: barrierLabel,
1425 barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
1426 settings: routeSettings,
1427 anchorPoint: anchorPoint,
1428 requestFocus: requestFocus,
1429 ),
1430 );
1431}
1432
1433/// A dialog route that shows an iOS-style dialog.
1434///
1435/// It is used internally by [showCupertinoDialog] or can be directly pushed
1436/// onto the [Navigator] stack to enable state restoration. See
1437/// [showCupertinoDialog] for a state restoration app example.
1438///
1439/// This function takes a `builder` which typically builds a [Dialog] widget.
1440/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
1441/// returned by the `builder` does not share a context with the location that
1442/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
1443/// custom [StatefulWidget] if the dialog needs to update dynamically.
1444///
1445/// The `context` argument is used to look up
1446/// [CupertinoLocalizations.modalBarrierDismissLabel], which provides the
1447/// modal with a localized accessibility label that will be used for the
1448/// modal's barrier. However, a custom `barrierLabel` can be passed in as well.
1449///
1450/// The `barrierDismissible` argument is used to indicate whether tapping on the
1451/// barrier will dismiss the dialog. It is `true` by default and cannot be `null`.
1452///
1453/// The `barrierColor` argument is used to specify the color of the modal
1454/// barrier that darkens everything below the dialog. If `null`, then
1455/// [CupertinoDynamicColor.resolve] is used to compute the modal color.
1456///
1457/// The `settings` argument define the settings for this route. See
1458/// [RouteSettings] for details.
1459///
1460/// {@macro flutter.widgets.RawDialogRoute}
1461///
1462/// See also:
1463///
1464/// * [showCupertinoDialog], which is a way to display
1465/// an iOS-style dialog.
1466/// * [showGeneralDialog], which allows for customization of the dialog popup.
1467/// * [showDialog], which displays a Material dialog.
1468/// * [DisplayFeatureSubScreen], which documents the specifics of how
1469/// [DisplayFeature]s can split the screen into sub-screens.
1470class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
1471 /// A dialog route that shows an iOS-style dialog.
1472 CupertinoDialogRoute({
1473 required WidgetBuilder builder,
1474 required BuildContext context,
1475 super.barrierDismissible,
1476 Color? barrierColor,
1477 String? barrierLabel,
1478 // This transition duration was eyeballed comparing with iOS
1479 super.transitionDuration = const Duration(milliseconds: 250),
1480 this.transitionBuilder,
1481 super.settings,
1482 super.requestFocus,
1483 super.anchorPoint,
1484 }) : super(
1485 pageBuilder: (
1486 BuildContext context,
1487 Animation<double> animation,
1488 Animation<double> secondaryAnimation,
1489 ) {
1490 return builder(context);
1491 },
1492 transitionBuilder: transitionBuilder ?? _buildCupertinoDialogTransitions,
1493 barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel,
1494 barrierColor:
1495 barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
1496 );
1497
1498 /// Custom transition builder
1499 RouteTransitionsBuilder? transitionBuilder;
1500
1501 CurvedAnimation? _fadeAnimation;
1502
1503 @override
1504 Simulation createSimulation({required bool forward}) {
1505 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
1506 final double end = forward ? 1.0 : 0.0;
1507 return SpringSimulation(
1508 _kStandardSpring,
1509 controller!.value,
1510 end,
1511 0,
1512 tolerance: _kStandardTolerance,
1513 snapToEnd: true,
1514 );
1515 }
1516
1517 @override
1518 Widget buildTransitions(
1519 BuildContext context,
1520 Animation<double> animation,
1521 Animation<double> secondaryAnimation,
1522 Widget child,
1523 ) {
1524 if (transitionBuilder != null) {
1525 return super.buildTransitions(context, animation, secondaryAnimation, child);
1526 }
1527
1528 if (animation.status == AnimationStatus.reverse) {
1529 return FadeTransition(opacity: animation, child: child);
1530 }
1531 return FadeTransition(
1532 opacity: animation,
1533 child: ScaleTransition(scale: animation.drive(_dialogScaleTween), child: child),
1534 );
1535 }
1536
1537 @override
1538 void dispose() {
1539 _fadeAnimation?.dispose();
1540 super.dispose();
1541 }
1542
1543 // The curve and initial scale values were mostly eyeballed from iOS, however
1544 // they reuse the same animation curve that was modeled after native page
1545 // transitions.
1546 static final Tween<double> _dialogScaleTween = Tween<double>(begin: 1.3, end: 1.0);
1547}
1548

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com