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

Provided by KDAB

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