route.dart 50.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:math';
6
import 'dart:ui' show lerpDouble, ImageFilter;
7

8
import 'package:flutter/foundation.dart';
9 10
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
11 12
import 'package:flutter/widgets.dart';

13
import 'colors.dart';
14
import 'interface_level.dart';
15
import 'localizations.dart';
16

17 18
const double _kBackGestureWidth = 20.0;
const double _kMinFlingVelocity = 1.0; // Screen widths per second.
19

20 21 22 23 24 25 26 27
// An eyeballed value for the maximum time it takes for a page to animate forward
// if the user releases a page mid swipe.
const int _kMaxDroppedSwipePageForwardAnimationTime = 800; // Milliseconds.

// The maximum time for a page to get reset to it's original position if the
// user releases a page mid swipe.
const int _kMaxPageBackAnimationTime = 300; // Milliseconds.

28 29 30 31
/// Barrier color for a Cupertino modal barrier.
///
/// Extracted from https://developer.apple.com/design/resources/.
const Color kCupertinoModalBarrierColor = CupertinoDynamicColor.withBrightness(
32 33 34
  color: Color(0x33000000),
  darkColor: Color(0x7A000000),
);
35

36 37 38
// The duration of the transition used when a modal popup is shown.
const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335);

39
// Offset from offscreen to the right to fully on screen.
40
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
41 42
  begin: const Offset(1.0, 0.0),
  end: Offset.zero,
43 44
);

45
// Offset from fully on screen to 1/3 offscreen to the left.
46
final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>(
47 48
  begin: Offset.zero,
  end: const Offset(-1.0/3.0, 0.0),
49 50
);

51
// Offset from offscreen below to fully on screen.
52
final Animatable<Offset> _kBottomUpTween = Tween<Offset>(
53 54
  begin: const Offset(0.0, 1.0),
  end: Offset.zero,
55 56
);

57 58
/// A mixin that replaces the entire screen with an iOS transition for a
/// [PageRoute].
59
///
60
/// {@template flutter.cupertino.cupertinoRouteTransitionMixin}
61 62
/// The page slides in from the right and exits in reverse. The page also shifts
/// to the left in parallax when another page enters to cover it.
63
///
64 65
/// The page slides in from the bottom and exits in reverse with no parallax
/// effect for fullscreen dialogs.
66
/// {@endtemplate}
67
///
68 69
/// See also:
///
70
///  * [MaterialRouteTransitionMixin], which is a mixin that provides
71
///    platform-appropriate transitions for a [PageRoute].
72 73
///  * [CupertinoPageRoute], which is a [PageRoute] that leverages this mixin.
mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
74
  /// Builds the primary contents of the route.
75 76
  @protected
  Widget buildContent(BuildContext context);
77

78
  /// {@template flutter.cupertino.CupertinoRouteTransitionMixin.title}
79 80
  /// A title string for this route.
  ///
81
  /// Used to auto-populate [CupertinoNavigationBar] and
82 83
  /// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when
  /// one is not manually supplied.
84
  /// {@endtemplate}
85
  String? get title;
86

87
  ValueNotifier<String?>? _previousTitle;
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102

  /// The title string of the previous [CupertinoPageRoute].
  ///
  /// The [ValueListenable]'s value is readable after the route is installed
  /// onto a [Navigator]. The [ValueListenable] will also notify its listeners
  /// if the value changes (such as by replacing the previous route).
  ///
  /// The [ValueListenable] itself will be null before the route is installed.
  /// Its content value will be null if the previous route has no title or
  /// is not a [CupertinoPageRoute].
  ///
  /// See also:
  ///
  ///  * [ValueListenableBuilder], which can be used to listen and rebuild
  ///    widgets based on a ValueListenable.
103
  ValueListenable<String?> get previousTitle {
104 105 106 107
    assert(
      _previousTitle != null,
      'Cannot read the previousTitle for a route that has not yet been installed',
    );
108
    return _previousTitle!;
109 110 111
  }

  @override
112 113
  void didChangePrevious(Route<dynamic>? previousRoute) {
    final String? previousTitleString = previousRoute is CupertinoRouteTransitionMixin
114 115
      ? previousRoute.title
      : null;
116
    if (_previousTitle == null) {
117
      _previousTitle = ValueNotifier<String?>(previousTitleString);
118
    } else {
119
      _previousTitle!.value = previousTitleString;
120 121 122 123
    }
    super.didChangePrevious(previousRoute);
  }

124
  @override
125 126
  // A relatively rigorous eyeball estimation.
  Duration get transitionDuration => const Duration(milliseconds: 400);
127 128

  @override
129
  Color? get barrierColor => null;
130

131
  @override
132
  String? get barrierLabel => null;
133

134
  @override
135 136
  bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
    // Don't perform outgoing animation if the next route is a fullscreen dialog.
137
    return nextRoute is CupertinoRouteTransitionMixin && !nextRoute.fullscreenDialog;
138 139
  }

140 141 142
  /// True if an iOS-style back swipe pop gesture is currently underway for [route].
  ///
  /// This just check the route's [NavigatorState.userGestureInProgress].
143
  ///
144 145 146 147
  /// See also:
  ///
  ///  * [popGestureEnabled], which returns true if a user-triggered pop gesture
  ///    would be allowed.
148
  static bool isPopGestureInProgress(PageRoute<dynamic> route) {
149
    return route.navigator!.userGestureInProgress;
150
  }
151

152
  /// True if an iOS-style back swipe pop gesture is currently underway for this route.
153 154 155
  ///
  /// See also:
  ///
156 157 158 159 160
  ///  * [isPopGestureInProgress], which returns true if a Cupertino pop gesture
  ///    is currently underway for specific route.
  ///  * [popGestureEnabled], which returns true if a user-triggered pop gesture
  ///    would be allowed.
  bool get popGestureInProgress => isPopGestureInProgress(this);
161

162
  /// Whether a pop gesture can be started by the user.
163
  ///
164
  /// Returns true if the user can edge-swipe to a previous route.
165
  ///
166 167
  /// Returns false once [isPopGestureInProgress] is true, but
  /// [isPopGestureInProgress] can only become true if [popGestureEnabled] was
168
  /// true first.
169
  ///
170
  /// This should only be used between frames, not during build.
171 172 173
  bool get popGestureEnabled => _isPopGestureEnabled(this);

  static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
174 175 176 177 178 179 180 181
    // If there's nothing to go back to, then obviously we don't support
    // the back gesture.
    if (route.isFirst)
      return false;
    // If the route wouldn't actually pop if we popped it, then the gesture
    // would be really confusing (or would skip internal routes), so disallow it.
    if (route.willHandlePopInternally)
      return false;
182 183
    // If attempts to dismiss this route might be vetoed such as in a page
    // with forms, then do not allow the user to dismiss the route with a swipe.
184 185
    if (route.hasScopedWillPopCallback)
      return false;
186
    // Fullscreen dialogs aren't dismissible by back swipe.
187
    if (route.fullscreenDialog)
188 189
      return false;
    // If we're in an animation already, we cannot be manually swiped.
190
    if (route.animation!.status != AnimationStatus.completed)
191 192 193 194
      return false;
    // If we're being popped into, we also cannot be swiped until the pop above
    // it completes. This translates to our secondary animation being
    // dismissed.
195
    if (route.secondaryAnimation!.status != AnimationStatus.dismissed)
196 197
      return false;
    // If we're in a gesture already, we cannot start another.
198
    if (isPopGestureInProgress(route))
199
      return false;
200

201 202 203
    // Looks like a back gesture would be welcome!
    return true;
  }
204

205
  @override
206
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
207
    final Widget child = buildContent(context);
208
    final Widget result = Semantics(
209 210
      scopesRoute: true,
      explicitChildNodes: true,
211
      child: child,
212
    );
213
    assert(() {
214
      if (child == null) {
215 216 217 218
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('The builder for route "${settings.name}" returned null.'),
          ErrorDescription('Route builders must never return null.'),
        ]);
219 220
      }
      return true;
221
    }());
222 223
    return result;
  }
224

225
  // Called by _CupertinoBackGestureDetector when a pop ("back") drag start
xster's avatar
xster committed
226
  // gesture is detected. The returned controller handles all of the subsequent
227 228 229 230
  // drag events.
  static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) {
    assert(_isPopGestureEnabled(route));

231
    return _CupertinoBackGestureController<T>(
232 233
      navigator: route.navigator!,
      controller: route.controller!, // protected access
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
    );
  }

  /// Returns a [CupertinoFullscreenDialogTransition] if [route] is a full
  /// screen dialog, otherwise a [CupertinoPageTransition] is returned.
  ///
  /// Used by [CupertinoPageRoute.buildTransitions].
  ///
  /// This method can be applied to any [PageRoute], not just
  /// [CupertinoPageRoute]. It's typically used to provide a Cupertino style
  /// horizontal transition for material widgets when the target platform
  /// is [TargetPlatform.iOS].
  ///
  /// See also:
  ///
  ///  * [CupertinoPageTransitionsBuilder], which uses this method to define a
  ///    [PageTransitionsBuilder] for the [PageTransitionsTheme].
  static Widget buildPageTransitions<T>(
    PageRoute<T> route,
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
258 259 260 261 262 263
    // Check if the route has an animation that's currently participating
    // in a back swipe gesture.
    //
    // In the middle of a back gesture drag, let the transition be linear to
    // match finger motions.
    final bool linearTransition = isPopGestureInProgress(route);
264
    if (route.fullscreenDialog) {
265
      return CupertinoFullscreenDialogTransition(
266 267
        primaryRouteAnimation: animation,
        secondaryRouteAnimation: secondaryAnimation,
268
        child: child,
269
        linearTransition: linearTransition,
270
      );
271
    } else {
272
      return CupertinoPageTransition(
273 274
        primaryRouteAnimation: animation,
        secondaryRouteAnimation: secondaryAnimation,
275
        linearTransition: linearTransition,
276
        child: _CupertinoBackGestureDetector<T>(
277 278
          enabledCallback: () => _isPopGestureEnabled<T>(route),
          onStartPopGesture: () => _startPopGesture<T>(route),
279 280
          child: child,
        ),
281
      );
282
    }
283
  }
284

285 286 287 288
  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child);
  }
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
}

/// A modal route that replaces the entire screen with an iOS transition.
///
/// {@macro flutter.cupertino.cupertinoRouteTransitionMixin}
///
/// By default, when a modal route is replaced by another, the previous route
/// remains in memory. To free all the resources when this is not necessary, set
/// [maintainState] to false.
///
/// The type `T` specifies the return type of the route which can be supplied as
/// the route is popped from the stack via [Navigator.pop] when an optional
/// `result` can be provided.
///
/// See also:
///
///  * [CupertinoRouteTransitionMixin], for a mixin that provides iOS transition
///    for this modal route.
///  * [MaterialPageRoute], for an adaptive [PageRoute] that uses a
///    platform-appropriate transition.
///  * [CupertinoPageScaffold], for applications that have one page with a fixed
///    navigation bar on top.
///  * [CupertinoTabScaffold], for applications that have a tab bar at the
///    bottom with multiple pages.
///  * [CupertinoPage], for a [Page] version of this class.
class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> {
  /// Creates a page route for use in an iOS designed app.
  ///
  /// The [builder], [maintainState], and [fullscreenDialog] arguments must not
  /// be null.
  CupertinoPageRoute({
320
    required this.builder,
321
    this.title,
322
    RouteSettings? settings,
323 324 325 326 327
    this.maintainState = true,
    bool fullscreenDialog = false,
  }) : assert(builder != null),
       assert(maintainState != null),
       assert(fullscreenDialog != null),
328 329 330
       super(settings: settings, fullscreenDialog: fullscreenDialog) {
    assert(opaque);
  }
331

332
  /// Builds the primary contents of the route.
333 334
  final WidgetBuilder builder;

335 336 337
  @override
  Widget buildContent(BuildContext context) => builder(context);

338
  @override
339
  final String? title;
340 341 342

  @override
  final bool maintainState;
343

344 345
  @override
  String get debugLabel => '${super.debugLabel}(${settings.name})';
346 347
}

348 349 350 351 352 353
// A page-based version of CupertinoPageRoute.
//
// This route uses the builder from the page to build its content. This ensures
// the content is up to date after page updates.
class _PageBasedCupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> {
  _PageBasedCupertinoPageRoute({
354
    required CupertinoPage<T> page,
355
  }) : assert(page != null),
356 357 358
       super(settings: page) {
    assert(opaque);
  }
359 360 361 362

  CupertinoPage<T> get _page => settings as CupertinoPage<T>;

  @override
363
  Widget buildContent(BuildContext context) => _page.child;
364 365

  @override
366
  String? get title => _page.title;
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396

  @override
  bool get maintainState => _page.maintainState;

  @override
  bool get fullscreenDialog => _page.fullscreenDialog;

  @override
  String get debugLabel => '${super.debugLabel}(${_page.name})';
}

/// A page that creates a cupertino style [PageRoute].
///
/// {@macro flutter.cupertino.cupertinoRouteTransitionMixin}
///
/// By default, when a created modal route is replaced by another, the previous
/// route remains in memory. To free all the resources when this is not
/// necessary, set [maintainState] to false.
///
/// The type `T` specifies the return type of the route which can be supplied as
/// the route is popped from the stack via [Navigator.transitionDelegate] by
/// providing the optional `result` argument to the
/// [RouteTransitionRecord.markForPop] in the [TransitionDelegate.resolve].
///
/// See also:
///
///  * [CupertinoPageRoute], for a [PageRoute] version of this class.
class CupertinoPage<T> extends Page<T> {
  /// Creates a cupertino page.
  const CupertinoPage({
397
    required this.child,
398 399 400
    this.maintainState = true,
    this.title,
    this.fullscreenDialog = false,
401 402 403
    LocalKey? key,
    String? name,
    Object? arguments,
404
    String? restorationId,
405
  }) : assert(child != null),
406 407
       assert(maintainState != null),
       assert(fullscreenDialog != null),
408
       super(key: key, name: name, arguments: arguments, restorationId: restorationId);
409

410 411
  /// The content to be shown in the [Route] created by this page.
  final Widget child;
412

413
  /// {@macro flutter.cupertino.CupertinoRouteTransitionMixin.title}
414
  final String? title;
415

416
  /// {@macro flutter.widgets.ModalRoute.maintainState}
417 418
  final bool maintainState;

419
  /// {@macro flutter.widgets.PageRoute.fullscreenDialog}
420 421 422 423 424 425 426 427
  final bool fullscreenDialog;

  @override
  Route<T> createRoute(BuildContext context) {
    return _PageBasedCupertinoPageRoute<T>(page: this);
  }
}

428
/// Provides an iOS-style page transition animation.
429 430 431
///
/// The page slides in from the right and exits in reverse. It also shifts to the left in
/// a parallax motion when another page enters to cover it.
432
class CupertinoPageTransition extends StatelessWidget {
433 434 435 436 437 438
  /// Creates an iOS-style page transition.
  ///
  ///  * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0
  ///    when this screen is being pushed.
  ///  * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0
  ///    when another screen is being pushed on top of this one.
439
  ///  * `linearTransition` is whether to perform the transitions linearly.
440
  ///    Used to precisely track back gesture drags.
441
  CupertinoPageTransition({
442 443 444 445 446
    Key? key,
    required Animation<double> primaryRouteAnimation,
    required Animation<double> secondaryRouteAnimation,
    required this.child,
    required bool linearTransition,
Ian Hickson's avatar
Ian Hickson committed
447
  }) : assert(linearTransition != null),
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
       _primaryPositionAnimation =
           (linearTransition
             ? primaryRouteAnimation
             : CurvedAnimation(
                 // The curves below have been rigorously derived from plots of native
                 // iOS animation frames. Specifically, a video was taken of a page
                 // transition animation and the distance in each frame that the page
                 // moved was measured. A best fit bezier curve was the fitted to the
                 // point set, which is linearToEaseIn. Conversely, easeInToLinear is the
                 // reflection over the origin of linearToEaseIn.
                 parent: primaryRouteAnimation,
                 curve: Curves.linearToEaseOut,
                 reverseCurve: Curves.easeInToLinear,
               )
           ).drive(_kRightMiddleTween),
       _secondaryPositionAnimation =
           (linearTransition
             ? secondaryRouteAnimation
             : CurvedAnimation(
                 parent: secondaryRouteAnimation,
                 curve: Curves.linearToEaseOut,
                 reverseCurve: Curves.easeInToLinear,
               )
           ).drive(_kMiddleLeftTween),
       _primaryShadowAnimation =
           (linearTransition
             ? primaryRouteAnimation
             : CurvedAnimation(
                 parent: primaryRouteAnimation,
                 curve: Curves.linearToEaseOut,
               )
479
           ).drive(_CupertinoEdgeShadowDecoration.kTween),
Ian Hickson's avatar
Ian Hickson committed
480
       super(key: key);
481 482

  // When this page is coming in to cover another page.
483
  final Animation<Offset> _primaryPositionAnimation;
484
  // When this page is becoming covered by another page.
485
  final Animation<Offset> _secondaryPositionAnimation;
486
  final Animation<Decoration> _primaryShadowAnimation;
487 488

  /// The widget below this widget in the tree.
489 490 491 492
  final Widget child;

  @override
  Widget build(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
493
    assert(debugCheckHasDirectionality(context));
494
    final TextDirection textDirection = Directionality.of(context);
495
    return SlideTransition(
496
      position: _secondaryPositionAnimation,
Ian Hickson's avatar
Ian Hickson committed
497
      textDirection: textDirection,
498
      transformHitTests: false,
499
      child: SlideTransition(
500
        position: _primaryPositionAnimation,
Ian Hickson's avatar
Ian Hickson committed
501
        textDirection: textDirection,
502
        child: DecoratedBoxTransition(
503
          decoration: _primaryShadowAnimation,
504 505 506
          child: child,
        ),
      ),
507 508 509 510
    );
  }
}

511 512 513 514
/// An iOS-style transition used for summoning fullscreen dialogs.
///
/// For example, used when creating a new calendar event by bringing in the next
/// screen from the bottom.
515
class CupertinoFullscreenDialogTransition extends StatelessWidget {
516
  /// Creates an iOS-style transition used for summoning fullscreen dialogs.
517 518 519 520 521 522 523
  ///
  ///  * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0
  ///    when this screen is being pushed.
  ///  * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0
  ///    when another screen is being pushed on top of this one.
  ///  * `linearTransition` is whether to perform the secondary transition linearly.
  ///    Used to precisely track back gesture drags.
524
  CupertinoFullscreenDialogTransition({
525 526 527 528 529
    Key? key,
    required Animation<double> primaryRouteAnimation,
    required Animation<double> secondaryRouteAnimation,
    required this.child,
    required bool linearTransition,
530
  }) : _positionAnimation = CurvedAnimation(
531
         parent: primaryRouteAnimation,
532 533 534 535 536
         curve: Curves.linearToEaseOut,
         // The curve must be flipped so that the reverse animation doesn't play
         // an ease-in curve, which iOS does not use.
         reverseCurve: Curves.linearToEaseOut.flipped,
       ).drive(_kBottomUpTween),
537 538 539 540 541 542 543 544 545
       _secondaryPositionAnimation =
           (linearTransition
             ? secondaryRouteAnimation
             : CurvedAnimation(
                 parent: secondaryRouteAnimation,
                 curve: Curves.linearToEaseOut,
                 reverseCurve: Curves.easeInToLinear,
               )
           ).drive(_kMiddleLeftTween),
546
       super(key: key);
547

548
  final Animation<Offset> _positionAnimation;
549 550
  // When this page is becoming covered by another page.
  final Animation<Offset> _secondaryPositionAnimation;
551 552

  /// The widget below this widget in the tree.
553
  final Widget child;
554 555

  @override
556
  Widget build(BuildContext context) {
557
    assert(debugCheckHasDirectionality(context));
558
    final TextDirection textDirection = Directionality.of(context);
559
    return SlideTransition(
560 561 562 563 564 565 566
      position: _secondaryPositionAnimation,
      textDirection: textDirection,
      transformHitTests: false,
      child: SlideTransition(
        position: _positionAnimation,
        child: child,
      ),
567
    );
568 569 570
  }
}

571 572 573 574 575
/// This is the widget side of [_CupertinoBackGestureController].
///
/// This widget provides a gesture recognizer which, when it determines the
/// route can be closed with a back gesture, creates the controller and
/// feeds it the input from the gesture recognizer.
Ian Hickson's avatar
Ian Hickson committed
576 577 578
///
/// The gesture data is converted from absolute coordinates to logical
/// coordinates by this widget.
579 580 581 582
///
/// The type `T` specifies the return type of the route with which this gesture
/// detector is associated.
class _CupertinoBackGestureDetector<T> extends StatefulWidget {
583
  const _CupertinoBackGestureDetector({
584 585 586 587
    Key? key,
    required this.enabledCallback,
    required this.onStartPopGesture,
    required this.child,
588 589 590 591 592 593 594 595 596
  }) : assert(enabledCallback != null),
       assert(onStartPopGesture != null),
       assert(child != null),
       super(key: key);

  final Widget child;

  final ValueGetter<bool> enabledCallback;

597
  final ValueGetter<_CupertinoBackGestureController<T>> onStartPopGesture;
598 599

  @override
600
  _CupertinoBackGestureDetectorState<T> createState() => _CupertinoBackGestureDetectorState<T>();
601 602
}

603
class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureDetector<T>> {
604
  _CupertinoBackGestureController<T>? _backGestureController;
605

606
  late HorizontalDragGestureRecognizer _recognizer;
607 608 609 610

  @override
  void initState() {
    super.initState();
611
    _recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
      ..onEnd = _handleDragEnd
      ..onCancel = _handleDragCancel;
  }

  @override
  void dispose() {
    _recognizer.dispose();
    super.dispose();
  }

  void _handleDragStart(DragStartDetails details) {
    assert(mounted);
    assert(_backGestureController == null);
    _backGestureController = widget.onStartPopGesture();
  }

  void _handleDragUpdate(DragUpdateDetails details) {
    assert(mounted);
    assert(_backGestureController != null);
633
    _backGestureController!.dragUpdate(_convertToLogical(details.primaryDelta! / context.size!.width));
634 635 636 637 638
  }

  void _handleDragEnd(DragEndDetails details) {
    assert(mounted);
    assert(_backGestureController != null);
639
    _backGestureController!.dragEnd(_convertToLogical(details.velocity.pixelsPerSecond.dx / context.size!.width));
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655
    _backGestureController = null;
  }

  void _handleDragCancel() {
    assert(mounted);
    // This can be called even if start is not called, paired with the "down" event
    // that we don't consider here.
    _backGestureController?.dragEnd(0.0);
    _backGestureController = null;
  }

  void _handlePointerDown(PointerDownEvent event) {
    if (widget.enabledCallback())
      _recognizer.addPointer(event);
  }

Ian Hickson's avatar
Ian Hickson committed
656
  double _convertToLogical(double value) {
657
    switch (Directionality.of(context)) {
Ian Hickson's avatar
Ian Hickson committed
658 659 660 661 662 663 664
      case TextDirection.rtl:
        return -value;
      case TextDirection.ltr:
        return value;
    }
  }

665 666
  @override
  Widget build(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
667
    assert(debugCheckHasDirectionality(context));
668 669 670
    // For devices with notches, the drag area needs to be larger on the side
    // that has the notch.
    double dragAreaWidth = Directionality.of(context) == TextDirection.ltr ?
671 672
                           MediaQuery.of(context).padding.left :
                           MediaQuery.of(context).padding.right;
673
    dragAreaWidth = max(dragAreaWidth, _kBackGestureWidth);
674
    return Stack(
675 676 677
      fit: StackFit.passthrough,
      children: <Widget>[
        widget.child,
678
        PositionedDirectional(
Ian Hickson's avatar
Ian Hickson committed
679
          start: 0.0,
680
          width: dragAreaWidth,
681 682
          top: 0.0,
          bottom: 0.0,
683
          child: Listener(
684 685 686
            onPointerDown: _handlePointerDown,
            behavior: HitTestBehavior.translucent,
          ),
687 688 689 690 691 692
        ),
      ],
    );
  }
}

693 694
/// A controller for an iOS-style back gesture.
///
695 696 697 698
/// This is created by a [CupertinoPageRoute] in response from a gesture caught
/// by a [_CupertinoBackGestureDetector] widget, which then also feeds it input
/// from the gesture. It controls the animation controller owned by the route,
/// based on the input provided by the gesture detector.
Ian Hickson's avatar
Ian Hickson committed
699 700 701
///
/// This class works entirely in logical coordinates (0.0 is new page dismissed,
/// 1.0 is new page on top).
702 703 704 705
///
/// The type `T` specifies the return type of the route with which this gesture
/// detector controller is associated.
class _CupertinoBackGestureController<T> {
706 707 708
  /// Creates a controller for an iOS-style back gesture.
  ///
  /// The [navigator] and [controller] arguments must not be null.
709
  _CupertinoBackGestureController({
710 711
    required this.navigator,
    required this.controller,
712 713 714
  }) : assert(navigator != null),
       assert(controller != null) {
    navigator.didStartUserGesture();
715 716
  }

717
  final AnimationController controller;
718
  final NavigatorState navigator;
719 720 721

  /// The drag gesture has changed by [fractionalDelta]. The total range of the
  /// drag should be 0.0 to 1.0.
722 723 724 725
  void dragUpdate(double delta) {
    controller.value -= delta;
  }

726 727 728 729 730 731
  /// The drag gesture has ended with a horizontal motion of
  /// [fractionalVelocity] as a fraction of screen width per second.
  void dragEnd(double velocity) {
    // Fling in the appropriate direction.
    // AnimationController.fling is guaranteed to
    // take at least one frame.
732 733 734 735
    //
    // This curve has been determined through rigorously eyeballing native iOS
    // animations.
    const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
736
    final bool animateForward;
737 738 739 740 741

    // If the user releases the page before mid screen with sufficient velocity,
    // or after mid screen, we should animate the page out. Otherwise, the page
    // should be animated back in.
    if (velocity.abs() >= _kMinFlingVelocity)
742
      animateForward = velocity <= 0;
743
    else
744
      animateForward = controller.value > 0.5;
745

746 747 748 749
    if (animateForward) {
      // The closer the panel is to dismissing, the shorter the animation is.
      // We want to cap the animation time, but we want to use a linear curve
      // to determine it.
750
      final int droppedPageForwardAnimationTime = min(
751
        lerpDouble(_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)!.floor(),
752 753
        _kMaxPageBackAnimationTime,
      );
754
      controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve);
755
    } else {
756 757 758 759 760 761
      // This route is destined to pop at this point. Reuse navigator's pop.
      navigator.pop();

      // The popping may have finished inline if already at the target destination.
      if (controller.isAnimating) {
        // Otherwise, use a custom popping animation duration and curve.
762
        final int droppedPageBackAnimationTime = lerpDouble(0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)!.floor();
763 764
        controller.animateBack(0.0, duration: Duration(milliseconds: droppedPageBackAnimationTime), curve: animationCurve);
      }
765
    }
766

767
    if (controller.isAnimating) {
768 769 770
      // Keep the userGestureInProgress in true state so we don't change the
      // curve of the page transition mid-flight since CupertinoPageTransition
      // depends on userGestureInProgress.
771
      late AnimationStatusListener animationStatusCallback;
772 773 774 775 776
      animationStatusCallback = (AnimationStatus status) {
        navigator.didStopUserGesture();
        controller.removeStatusListener(animationStatusCallback);
      };
      controller.addStatusListener(animationStatusCallback);
777 778
    } else {
      navigator.didStopUserGesture();
779
    }
780 781
  }
}
782

Ian Hickson's avatar
Ian Hickson committed
783 784 785
// A custom [Decoration] used to paint an extra shadow on the start edge of the
// box it's decorating. It's like a [BoxDecoration] with only a gradient except
// it paints on the start side of the box instead of behind the box.
786
class _CupertinoEdgeShadowDecoration extends Decoration {
787
  const _CupertinoEdgeShadowDecoration._([this._colors]);
788

789 790 791 792 793 794 795 796 797 798 799 800
  static DecorationTween kTween = DecorationTween(
    begin: const _CupertinoEdgeShadowDecoration._(), // No decoration initially.
    end: const _CupertinoEdgeShadowDecoration._(
      // Eyeballed gradient used to mimic a drop shadow on the start side only.
      <Color>[
        Color(0x38000000),
        Color(0x12000000),
        Color(0x04000000),
        Color(0x00000000),
      ],
    ),
  );
801

802 803 804 805 806 807 808 809 810 811 812
  // Colors used to paint a gradient at the start edge of the box it is
  // decorating.
  //
  // The first color in the list is used at the start of the gradient, which
  // is located at the start edge of the decorated box.
  //
  // If this is null, no shadow is drawn.
  //
  // The list must have at least two colors in it (otherwise it would not be a
  // gradient).
  final List<Color>? _colors;
813

814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830
  // Linearly interpolate between two edge shadow decorations decorations.
  //
  // The `t` argument represents position on the timeline, with 0.0 meaning
  // that the interpolation has not started, returning `a` (or something
  // equivalent to `a`), 1.0 meaning that the interpolation has finished,
  // returning `b` (or something equivalent to `b`), and values in between
  // meaning that the interpolation is at the relevant point on the timeline
  // between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
  // 1.0, so negative values and values greater than 1.0 are valid (and can
  // easily be generated by curves such as [Curves.elasticInOut]).
  //
  // Values for `t` are usually obtained from an [Animation<double>], such as
  // an [AnimationController].
  //
  // See also:
  //
  //  * [Decoration.lerp].
831 832 833
  static _CupertinoEdgeShadowDecoration? lerp(
    _CupertinoEdgeShadowDecoration? a,
    _CupertinoEdgeShadowDecoration? b,
834
    double t,
835
  ) {
836
    assert(t != null);
837 838
    if (a == null && b == null)
      return null;
839 840 841 842 843 844 845 846 847 848 849 850 851
    if (a == null)
      return b!._colors == null ? b : _CupertinoEdgeShadowDecoration._(b._colors!.map<Color>((Color color) => Color.lerp(null, color, t)!).toList());
    if (b == null)
      return a._colors == null ? a : _CupertinoEdgeShadowDecoration._(a._colors!.map<Color>((Color color) => Color.lerp(null, color, 1.0 - t)!).toList());
    assert(b._colors != null || a._colors != null);
    // If it ever becomes necessary, we could allow decorations with different
    // length' here, similarly to how it is handled in [LinearGradient.lerp].
    assert(b._colors == null || a._colors == null || a._colors!.length == b._colors!.length);
    return _CupertinoEdgeShadowDecoration._(
      <Color>[
        for (int i = 0; i < b._colors!.length; i += 1)
          Color.lerp(a._colors?[i], b._colors?[i], t)!,
      ]
852 853 854 855
    );
  }

  @override
856
  _CupertinoEdgeShadowDecoration lerpFrom(Decoration? a, double t) {
857
    if (a is _CupertinoEdgeShadowDecoration)
858 859
      return _CupertinoEdgeShadowDecoration.lerp(a, this, t)!;
    return _CupertinoEdgeShadowDecoration.lerp(null, this, t)!;
860 861 862
  }

  @override
863
  _CupertinoEdgeShadowDecoration lerpTo(Decoration? b, double t) {
864
    if (b is _CupertinoEdgeShadowDecoration)
865 866
      return _CupertinoEdgeShadowDecoration.lerp(this, b, t)!;
    return _CupertinoEdgeShadowDecoration.lerp(this, null, t)!;
867 868 869
  }

  @override
870
  _CupertinoEdgeShadowPainter createBoxPainter([ VoidCallback? onChanged ]) {
871
    return _CupertinoEdgeShadowPainter(this, onChanged);
872 873 874
  }

  @override
875 876
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
877
      return false;
878
    return other is _CupertinoEdgeShadowDecoration
879
        && other._colors == _colors;
880 881 882
  }

  @override
883
  int get hashCode => _colors.hashCode;
884 885

  @override
886 887
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
888
    properties.add(IterableProperty<Color>('colors', _colors));
889
  }
890 891 892 893 894 895
}

/// A [BoxPainter] used to draw the page transition shadow using gradients.
class _CupertinoEdgeShadowPainter extends BoxPainter {
  _CupertinoEdgeShadowPainter(
    this._decoration,
896
    VoidCallback? onChange,
897
  ) : assert(_decoration != null),
898
      assert(_decoration._colors == null || _decoration._colors!.length > 1),
899 900 901 902 903 904
      super(onChange);

  final _CupertinoEdgeShadowDecoration _decoration;

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
905 906
    final List<Color>? colors = _decoration._colors;
    if (colors == null) {
907
      return;
908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937
    }

    // The following code simulates drawing a [LinearGradient] configured as
    // follows:
    //
    // LinearGradient(
    //   begin: AlignmentDirectional(0.90, 0.0), // Spans 5% of the page.
    //   colors: _decoration._colors,
    // )
    //
    // A performance evaluation on Feb 8, 2021 showed, that drawing the gradient
    // manually as implemented below is more performant than relying on
    // [LinearGradient.createShader] because compiling that shader takes a long
    // time. On an iPhone XR, the implementation below reduced the worst frame
    // time for a cupertino page transition of a newly installed app from ~95ms
    // down to ~30ms, mainly because there's no longer a need to compile a
    // shader for the LinearGradient.
    //
    // The implementation below divides the width of the shadow into multiple
    // bands of equal width, one for each color interval defined by
    // `_decoration._colors`. Band x is filled with a gradient going from
    // `_decoration._colors[x]` to `_decoration._colors[x + 1]` by drawing a
    // bunch of 1px wide rects. The rects change their color by lerping between
    // the two colors that define the interval of the band.

    // Shadow spans 5% of the page.
    final double shadowWidth = 0.05 * configuration.size!.width;
    final double shadowHeight = configuration.size!.height;
    final double bandWidth = shadowWidth / (colors.length - 1);

938
    final TextDirection? textDirection = configuration.textDirection;
Ian Hickson's avatar
Ian Hickson committed
939
    assert(textDirection != null);
940 941
    final double start;
    final double shadowDirection; // -1 for ltr, 1 for rtl.
942
    switch (textDirection!) {
Ian Hickson's avatar
Ian Hickson committed
943
      case TextDirection.rtl:
944 945
        start = offset.dx + configuration.size!.width;
        shadowDirection = 1;
Ian Hickson's avatar
Ian Hickson committed
946 947
        break;
      case TextDirection.ltr:
948 949
        start = offset.dx;
        shadowDirection = -1;
Ian Hickson's avatar
Ian Hickson committed
950 951
        break;
    }
952

953 954 955 956 957 958 959 960 961 962
    int bandColorIndex = 0;
    for (int dx = 0; dx < shadowWidth; dx += 1) {
      if (dx ~/ bandWidth != bandColorIndex) {
        bandColorIndex += 1;
      }
      final Paint paint = Paint()
        ..color = Color.lerp(colors[bandColorIndex], colors[bandColorIndex + 1], (dx % bandWidth) / bandWidth)!;
      final double x = start + shadowDirection * dx;
      canvas.drawRect(Rect.fromLTWH(x - 1.0, offset.dy, 1.0, shadowHeight), paint);
    }
963 964
  }
}
965

966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999
/// A route that shows a modal iOS-style popup that slides up from the
/// bottom of the screen.
///
/// Such a popup is an alternative to a menu or a dialog and prevents the user
/// from interacting with the rest of the app.
///
/// It is used internally by [showCupertinoModalPopup] or can be directly pushed
/// onto the [Navigator] stack to enable state restoration. See
/// [showCupertinoModalPopup] for a state restoration app example.
///
/// The `barrierColor` argument determines the [Color] of the barrier underneath
/// the popup. When unspecified, the barrier color defaults to a light opacity
/// black scrim based on iOS's dialog screens. To correctly have iOS resolve
/// to the appropriate modal colors, pass in
/// `CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context)`.
///
/// The `barrierDismissible` argument determines whether clicking outside the
/// popup results in dismissal. It is `true` by default.
///
/// The `semanticsDismissible` argument is used to determine whether the
/// semantics of the modal barrier are included in the semantics tree.
///
/// The `routeSettings` argument is used to provide [RouteSettings] to the
/// created Route.
///
/// See also:
///
///  * [CupertinoActionSheet], which is the widget usually returned by the
///    `builder` argument.
///  * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
class CupertinoModalPopupRoute<T> extends PopupRoute<T> {
  /// A route that shows a modal iOS-style popup that slides up from the
  /// bottom of the screen.
  CupertinoModalPopupRoute({
1000
    required this.builder,
1001 1002 1003
    this.barrierLabel = 'Dismiss',
    this.barrierColor = kCupertinoModalBarrierColor,
    bool barrierDismissible = true,
1004
    bool? semanticsDismissible,
1005
    ImageFilter? filter,
1006
    RouteSettings? settings,
1007 1008 1009
  }) : super(
         filter: filter,
         settings: settings,
Dan Field's avatar
Dan Field committed
1010
       ) {
1011
    _barrierDismissible = barrierDismissible;
Dan Field's avatar
Dan Field committed
1012 1013
    _semanticsDismissible = semanticsDismissible;
  }
1014

1015 1016 1017 1018 1019 1020 1021 1022
  /// A builder that builds the widget tree for the [CupertinoModalPopupRoute].
  ///
  /// The `builder` argument typically builds a [CupertinoActionSheet] widget.
  ///
  /// Content below the widget is dimmed with a [ModalBarrier]. The widget built
  /// by the `builder` does not share a context with the route it was originally
  /// built from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the
  /// widget needs to update dynamically.
1023
  final WidgetBuilder builder;
1024 1025 1026

  bool? _barrierDismissible;

1027
  bool? _semanticsDismissible;
1028 1029 1030 1031 1032

  @override
  final String barrierLabel;

  @override
1033
  final Color? barrierColor;
1034 1035

  @override
1036
  bool get barrierDismissible => _barrierDismissible ?? true;
1037 1038

  @override
Dan Field's avatar
Dan Field committed
1039
  bool get semanticsDismissible => _semanticsDismissible ?? false;
1040 1041 1042 1043

  @override
  Duration get transitionDuration => _kModalPopupTransitionDuration;

1044
  Animation<double>? _animation;
1045

1046
  late Tween<Offset> _offsetTween;
1047 1048 1049 1050

  @override
  Animation<double> createAnimation() {
    assert(_animation == null);
1051
    _animation = CurvedAnimation(
1052
      parent: super.createAnimation(),
1053 1054 1055 1056 1057

      // These curves were initially measured from native iOS horizontal page
      // route animations and seemed to be a good match here as well.
      curve: Curves.linearToEaseOut,
      reverseCurve: Curves.linearToEaseOut.flipped,
1058
    );
1059
    _offsetTween = Tween<Offset>(
1060
      begin: const Offset(0.0, 1.0),
1061
      end: Offset.zero,
1062
    );
1063
    return _animation!;
1064 1065 1066 1067
  }

  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
1068 1069 1070 1071
    return CupertinoUserInterfaceLevel(
      data: CupertinoUserInterfaceLevelData.elevated,
      child: Builder(builder: builder),
    );
1072 1073 1074 1075
  }

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
1076
    return Align(
1077
      alignment: Alignment.bottomCenter,
1078
      child: FractionalTranslation(
1079
        translation: _offsetTween.evaluate(_animation!),
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094
        child: child,
      ),
    );
  }
}

/// Shows a modal iOS-style popup that slides up from the bottom of the screen.
///
/// Such a popup is an alternative to a menu or a dialog and prevents the user
/// from interacting with the rest of the app.
///
/// The `context` argument is used to look up the [Navigator] for the popup.
/// It is only used when the method is called. Its corresponding widget can be
/// safely removed from the tree before the popup is closed.
///
1095 1096 1097 1098 1099 1100 1101
/// The `barrierColor` argument determines the [Color] of the barrier underneath
/// the popup. When unspecified, the barrier color defaults to a light opacity
/// black scrim based on iOS's dialog screens.
///
/// The `barrierDismissible` argument determines whether clicking outside the
/// popup results in dismissal. It is `true` by default.
///
1102 1103 1104 1105
/// The `useRootNavigator` argument is used to determine whether to push the
/// popup to the [Navigator] furthest from or nearest to the given `context`. It
/// is `false` by default.
///
1106
/// The `semanticsDismissible` argument is used to determine whether the
Dan Field's avatar
Dan Field committed
1107 1108
/// semantics of the modal barrier are included in the semantics tree.
///
1109 1110 1111
/// The `routeSettings` argument is used to provide [RouteSettings] to the
/// created Route.
///
1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
/// The `builder` argument typically builds a [CupertinoActionSheet] widget.
/// Content below the widget is dimmed with a [ModalBarrier]. The widget built
/// by the `builder` does not share a context with the location that
/// `showCupertinoModalPopup` is originally called from. Use a
/// [StatefulBuilder] or a custom [StatefulWidget] if the widget needs to
/// update dynamically.
///
/// Returns a `Future` that resolves to the value that was passed to
/// [Navigator.pop] when the popup was closed.
///
1122 1123 1124 1125 1126 1127 1128 1129
/// ### State Restoration in Modals
///
/// Using this method will not enable state restoration for the modal. In order
/// to enable state restoration for a modal, use [Navigator.restorablePush]
/// or [Navigator.restorablePushNamed] with [CupertinoModalPopupRoute].
///
/// For more information about state restoration, see [RestorationManager].
///
1130
/// {@tool sample --template=stateless_widget_restoration_cupertino}
1131 1132 1133 1134 1135 1136 1137 1138 1139
///
/// This sample demonstrates how to create a restorable Cupertino modal route.
/// This is accomplished by enabling state restoration by specifying
/// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to
/// push [CupertinoModalPopupRoute] when the [CupertinoButton] is tapped.
///
/// {@macro flutter.widgets.RestorationManager}
///
/// ```dart
1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151
/// Widget build(BuildContext context) {
///   return CupertinoPageScaffold(
///     navigationBar: const CupertinoNavigationBar(
///       middle: Text('Home'),
///     ),
///     child: Center(child: CupertinoButton(
///       onPressed: () {
///         Navigator.of(context).restorablePush(_modalBuilder);
///       },
///       child: const Text('Open Modal'),
///     )),
///   );
1152 1153
/// }
///
1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176
/// static Route<void> _modalBuilder(BuildContext context, Object? arguments) {
///   return CupertinoModalPopupRoute<void>(
///     builder: (BuildContext context) {
///       return CupertinoActionSheet(
///         title: const Text('Title'),
///         message: const Text('Message'),
///         actions: <CupertinoActionSheetAction>[
///           CupertinoActionSheetAction(
///             child: const Text('Action One'),
///             onPressed: () {
///               Navigator.pop(context);
///             },
///           ),
///           CupertinoActionSheetAction(
///             child: const Text('Action Two'),
///             onPressed: () {
///               Navigator.pop(context);
///             },
///           ),
///         ],
///       );
///     },
///   );
1177 1178 1179 1180 1181
/// }
/// ```
///
/// {@end-tool}
///
1182 1183
/// See also:
///
Dan Field's avatar
Dan Field committed
1184 1185
///  * [CupertinoActionSheet], which is the widget usually returned by the
///    `builder` argument to [showCupertinoModalPopup].
1186
///  * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
1187
Future<T?> showCupertinoModalPopup<T>({
1188 1189 1190
  required BuildContext context,
  required WidgetBuilder builder,
  ImageFilter? filter,
1191
  Color barrierColor = kCupertinoModalBarrierColor,
1192
  bool barrierDismissible = true,
1193
  bool useRootNavigator = true,
1194
  bool? semanticsDismissible,
1195
  RouteSettings? routeSettings,
1196
}) {
1197
  assert(useRootNavigator != null);
1198
  return Navigator.of(context, rootNavigator: useRootNavigator).push(
1199
    CupertinoModalPopupRoute<T>(
1200 1201
      builder: builder,
      filter: filter,
1202 1203
      barrierColor: CupertinoDynamicColor.resolve(barrierColor, context),
      barrierDismissible: barrierDismissible,
Dan Field's avatar
Dan Field committed
1204
      semanticsDismissible: semanticsDismissible,
1205
      settings: routeSettings,
1206 1207 1208 1209
    ),
  );
}

1210
// The curve and initial scale values were mostly eyeballed from iOS, however
1211
// they reuse the same animation curve that was modeled after native page
1212 1213 1214
// transitions.
final Animatable<double> _dialogScaleTween = Tween<double>(begin: 1.3, end: 1.0)
  .chain(CurveTween(curve: Curves.linearToEaseOut));
1215

1216
Widget _buildCupertinoDialogTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
1217
  final CurvedAnimation fadeAnimation = CurvedAnimation(
1218 1219 1220 1221
    parent: animation,
    curve: Curves.easeInOut,
  );
  if (animation.status == AnimationStatus.reverse) {
1222
    return FadeTransition(
1223 1224 1225 1226
      opacity: fadeAnimation,
      child: child,
    );
  }
1227
  return FadeTransition(
1228 1229 1230
    opacity: fadeAnimation,
    child: ScaleTransition(
      child: child,
1231
      scale: animation.drive(_dialogScaleTween),
1232 1233 1234 1235 1236 1237
    ),
  );
}

/// Displays an iOS-style dialog above the current contents of the app, with
/// iOS-style entrance and exit animations, modal barrier color, and modal
1238 1239
/// barrier behavior (by default, the dialog is not dismissible with a tap on
/// the barrier).
1240
///
1241 1242 1243 1244 1245
/// This function takes a `builder` which typically builds a [CupertinoAlertDialog]
/// widget. Content below the dialog is dimmed with a [ModalBarrier]. The widget
/// returned by the `builder` does not share a context with the location that
/// `showCupertinoDialog` is originally called from. Use a [StatefulBuilder] or
/// a custom [StatefulWidget] if the dialog needs to update dynamically.
1246 1247 1248 1249 1250
///
/// The `context` argument is used to look up the [Navigator] for the dialog.
/// It is only used when the method is called. Its corresponding widget can
/// be safely removed from the tree before the dialog is closed.
///
1251 1252 1253 1254
/// The `useRootNavigator` argument is used to determine whether to push the
/// dialog to the [Navigator] furthest from or nearest to the given `context`.
/// By default, `useRootNavigator` is `true` and the dialog route created by
/// this method is pushed to the root navigator.
1255 1256 1257 1258 1259
///
/// If the application has multiple [Navigator] objects, it may be necessary to
/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
/// dialog rather than just `Navigator.pop(context, result)`.
///
1260 1261 1262
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
1263 1264 1265 1266 1267 1268 1269 1270
/// ### State Restoration in Dialogs
///
/// Using this method will not enable state restoration for the dialog. In order
/// to enable state restoration for a dialog, use [Navigator.restorablePush]
/// or [Navigator.restorablePushNamed] with [CupertinoDialogRoute].
///
/// For more information about state restoration, see [RestorationManager].
///
1271
/// {@tool sample --template=stateless_widget_restoration_cupertino}
1272 1273 1274 1275 1276 1277 1278 1279 1280
///
/// This sample demonstrates how to create a restorable Cupertino dialog. This is
/// accomplished by enabling state restoration by specifying
/// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to
/// push [CupertinoDialogRoute] when the [CupertinoButton] is tapped.
///
/// {@macro flutter.widgets.RestorationManager}
///
/// ```dart
1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292
/// Widget build(BuildContext context) {
///   return CupertinoPageScaffold(
///     navigationBar: const CupertinoNavigationBar(
///       middle: Text('Home'),
///     ),
///     child: Center(child: CupertinoButton(
///       onPressed: () {
///         Navigator.of(context).restorablePush(_dialogBuilder);
///       },
///       child: const Text('Open Dialog'),
///     )),
///   );
1293 1294
/// }
///
1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308
/// static Route<Object?> _dialogBuilder(BuildContext context, Object? arguments) {
///   return CupertinoDialogRoute<void>(
///     context: context,
///     builder: (BuildContext context) {
///       return const CupertinoAlertDialog(
///         title: Text('Title'),
///         content: Text('Content'),
///         actions: <Widget>[
///           CupertinoDialogAction(child: Text('Yes')),
///           CupertinoDialogAction(child: Text('No')),
///         ],
///       );
///     },
///   );
1309 1310 1311 1312 1313
/// }
/// ```
///
/// {@end-tool}
///
1314
/// See also:
1315
///
1316 1317 1318 1319
///  * [CupertinoAlertDialog], an iOS-style alert dialog.
///  * [showDialog], which displays a Material-style dialog.
///  * [showGeneralDialog], which allows for customization of the dialog popup.
///  * <https://developer.apple.com/ios/human-interface-guidelines/views/alerts/>
1320
Future<T?> showCupertinoDialog<T>({
1321 1322
  required BuildContext context,
  required WidgetBuilder builder,
1323
  String? barrierLabel,
1324
  bool useRootNavigator = true,
1325
  bool barrierDismissible = false,
1326
  RouteSettings? routeSettings,
1327 1328
}) {
  assert(builder != null);
1329
  assert(useRootNavigator != null);
1330 1331 1332

  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(CupertinoDialogRoute<T>(
    builder: builder,
1333
    context: context,
1334
    barrierDismissible: barrierDismissible,
1335
    barrierLabel: barrierLabel,
1336
    barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381
    settings: routeSettings,
  ));
}

/// A dialog route that shows an iOS-style dialog.
///
/// It is used internally by [showCupertinoDialog] or can be directly pushed
/// onto the [Navigator] stack to enable state restoration. See
/// [showCupertinoDialog] for a state restoration app example.
///
/// This function takes a `builder` which typically builds a [Dialog] widget.
/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
/// returned by the `builder` does not share a context with the location that
/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
/// custom [StatefulWidget] if the dialog needs to update dynamically.
///
/// The `context` argument is used to look up
/// [CupertinoLocalizations.modalBarrierDismissLabel], which provides the
/// modal with a localized accessibility label that will be used for the
/// modal's barrier. However, a custom `barrierLabel` can be passed in as well.
///
/// The `barrierDismissible` argument is used to indicate whether tapping on the
/// barrier will dismiss the dialog. It is `true` by default and cannot be `null`.
///
/// The `barrierColor` argument is used to specify the color of the modal
/// barrier that darkens everything below the dialog. If `null`, then
/// [CupertinoDynamicColor.resolve] is used to compute the modal color.
///
/// The `settings` argument define the settings for this route. See
/// [RouteSettings] for details.
///
/// See also:
///
///  * [showCupertinoDialog], which is a way to display
///     an iOS-style dialog.
///  * [showGeneralDialog], which allows for customization of the dialog popup.
///  * [showDialog], which displays a Material dialog.
class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
  /// A dialog route that shows an iOS-style dialog.
  CupertinoDialogRoute({
    required WidgetBuilder builder,
    required BuildContext context,
    bool barrierDismissible = true,
    Color? barrierColor,
    String? barrierLabel,
1382
    // This transition duration was eyeballed comparing with iOS
1383 1384 1385 1386 1387 1388 1389 1390 1391 1392
    Duration transitionDuration = const Duration(milliseconds: 250),
    RouteTransitionsBuilder? transitionBuilder = _buildCupertinoDialogTransitions,
    RouteSettings? settings,
  }) : assert(barrierDismissible != null),
      super(
        pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
          return builder(context);
        },
        barrierDismissible: barrierDismissible,
        barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel,
1393
        barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context),
1394 1395 1396 1397
        transitionDuration: transitionDuration,
        transitionBuilder: transitionBuilder,
        settings: settings,
      );
1398
}