route.dart 30.9 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';

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

12 13
const double _kBackGestureWidth = 20.0;
const double _kMinFlingVelocity = 1.0; // Screen widths per second.
14

15 16 17
// Barrier color for a Cupertino modal barrier.
const Color _kModalBarrierColor = Color(0x6604040F);

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

21
// Offset from offscreen to the right to fully on screen.
22
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
23 24
  begin: const Offset(1.0, 0.0),
  end: Offset.zero,
25 26
);

27
// Offset from fully on screen to 1/3 offscreen to the left.
28
final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>(
29 30
  begin: Offset.zero,
  end: const Offset(-1.0/3.0, 0.0),
31 32
);

33
// Offset from offscreen below to fully on screen.
34
final Animatable<Offset> _kBottomUpTween = Tween<Offset>(
35 36
  begin: const Offset(0.0, 1.0),
  end: Offset.zero,
37 38
);

39
// Custom decoration from no shadow to page shadow mimicking iOS page
40
// transitions using gradients.
41
final DecorationTween _kGradientShadowTween = DecorationTween(
42 43
  begin: _CupertinoEdgeShadowDecoration.none, // No decoration initially.
  end: const _CupertinoEdgeShadowDecoration(
44
    edgeGradient: LinearGradient(
45
      // Spans 5% of the page.
46
      begin: AlignmentDirectional(0.90, 0.0),
Ian Hickson's avatar
Ian Hickson committed
47 48
      end: AlignmentDirectional.centerEnd,
      // Eyeballed gradient used to mimic a drop shadow on the start side only.
49 50 51 52 53
      colors: <Color>[
        Color(0x00000000),
        Color(0x04000000),
        Color(0x12000000),
        Color(0x38000000)
54
      ],
55
      stops: <double>[0.0, 0.3, 0.6, 1.0],
56
    ),
57 58 59
  ),
);

60 61
/// A modal route that replaces the entire screen with an iOS transition.
///
62 63
/// 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.
64
///
65 66
/// The page slides in from the bottom and exits in reverse with no parallax
/// effect for fullscreen dialogs.
67 68 69 70 71
///
/// 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.
///
72 73 74 75
/// 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.
///
76 77
/// See also:
///
78 79 80 81 82 83
///  * [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.
84 85
class CupertinoPageRoute<T> extends PageRoute<T> {
  /// Creates a page route for use in an iOS designed app.
86
  ///
87 88
  /// The [builder], [maintainState], and [fullscreenDialog] arguments must not
  /// be null.
89 90
  CupertinoPageRoute({
    @required this.builder,
91
    this.title,
92
    RouteSettings settings,
93 94
    this.maintainState = true,
    bool fullscreenDialog = false,
95
  }) : assert(builder != null),
96 97
       assert(maintainState != null),
       assert(fullscreenDialog != null),
98
       super(settings: settings, fullscreenDialog: fullscreenDialog) {
99
    // ignore: prefer_asserts_in_initializer_lists, https://github.com/dart-lang/sdk/issues/31223
100 101
    assert(opaque); // PageRoute makes it return true.
  }
102 103 104

  /// Builds the primary contents of the route.
  final WidgetBuilder builder;
105

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
  /// A title string for this route.
  ///
  /// Used to autopopulate [CupertinoNavigationBar] and
  /// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when
  /// one is not manually supplied.
  final String title;

  ValueNotifier<String> _previousTitle;

  /// 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.
  ValueListenable<String> get previousTitle {
    assert(
      _previousTitle != null,
      'Cannot read the previousTitle for a route that has not yet been installed',
    );
    return _previousTitle;
  }

  @override
  void didChangePrevious(Route<dynamic> previousRoute) {
    final String previousTitleString = previousRoute is CupertinoPageRoute
        ? previousRoute.title
        : null;
    if (_previousTitle == null) {
143
      _previousTitle = ValueNotifier<String>(previousTitleString);
144 145 146 147 148 149
    } else {
      _previousTitle.value = previousTitleString;
    }
    super.didChangePrevious(previousRoute);
  }

150 151
  @override
  final bool maintainState;
152

153 154
  @override
  Duration get transitionDuration => const Duration(milliseconds: 350);
155 156

  @override
157
  Color get barrierColor => null;
158

159 160 161
  @override
  String get barrierLabel => null;

162
  @override
163 164
  bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) {
    return previousRoute is CupertinoPageRoute;
165
  }
166

167
  @override
168 169 170
  bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
    // Don't perform outgoing animation if the next route is a fullscreen dialog.
    return nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog;
171 172 173
  }

  @override
174
  void dispose() {
175
    _popGestureInProgress.remove(this);
176
    super.dispose();
177 178
  }

179
  /// True if a Cupertino pop gesture is currently underway for [route].
180
  ///
181 182 183 184 185 186 187 188
  /// See also:
  ///
  ///  * [popGestureEnabled], which returns true if a user-triggered pop gesture
  ///    would be allowed.
  static bool isPopGestureInProgress(PageRoute<dynamic> route) => _popGestureInProgress.contains(route);
  static final Set<PageRoute<dynamic>> _popGestureInProgress = Set<PageRoute<dynamic>>();

  /// True if a Cupertino pop gesture is currently underway for this route.
189 190 191
  ///
  /// See also:
  ///
192 193 194 195 196
  ///  * [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);
197

198
  /// Whether a pop gesture can be started by the user.
199
  ///
200
  /// Returns true if the user can edge-swipe to a previous route.
201
  ///
202 203
  /// Returns false once [isPopGestureInProgress] is true, but
  /// [isPopGestureInProgress] can only become true if [popGestureEnabled] was
204
  /// true first.
205
  ///
206
  /// This should only be used between frames, not during build.
207 208 209
  bool get popGestureEnabled => _isPopGestureEnabled(this);

  static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
210 211 212 213 214 215 216 217
    // 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;
218 219
    // 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.
220 221
    if (route.hasScopedWillPopCallback)
      return false;
222
    // Fullscreen dialogs aren't dismissable by back swipe.
223
    if (route.fullscreenDialog)
224 225 226 227 228
      return false;
    // If we're in an animation already, we cannot be manually swiped.
    if (route.controller.status != AnimationStatus.completed)
      return false;
    // If we're in a gesture already, we cannot start another.
229
    if (_popGestureInProgress.contains(route))
230
      return false;
231

232 233 234
    // Looks like a back gesture would be welcome!
    return true;
  }
235

236
  @override
237
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
238
    final Widget result = Semantics(
239 240 241 242
      scopesRoute: true,
      explicitChildNodes: true,
      child: builder(context),
    );
243 244
    assert(() {
      if (result == null) {
245
        throw FlutterError(
246 247 248 249 250
          'The builder for route "${settings.name}" returned null.\n'
          'Route builders must never return null.'
        );
      }
      return true;
251
    }());
252 253
    return result;
  }
254

255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
  // Called by _CupertinoBackGestureDetector when a pop ("back") drag start
  // gesture is detected. The returned controller handles all of the subsquent
  // drag events.
  static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) {
    assert(!_popGestureInProgress.contains(route));
    assert(_isPopGestureEnabled(route));
    _popGestureInProgress.add(route);

    _CupertinoBackGestureController<T> backController;
    backController = _CupertinoBackGestureController<T>(
      navigator: route.navigator,
      controller: route.controller,
      onEnded: () {
        backController?.dispose();
        backController = null;
        _popGestureInProgress.remove(route);
      },
    );
    return backController;
  }

  /// 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,
  ) {
    if (route.fullscreenDialog) {
298
      return CupertinoFullscreenDialogTransition(
299 300 301
        animation: animation,
        child: child,
      );
302
    } else {
303
      return CupertinoPageTransition(
304 305
        primaryRouteAnimation: animation,
        secondaryRouteAnimation: secondaryAnimation,
306 307
        // In the middle of a back gesture drag, let the transition be linear to
        // match finger motions.
308
        linearTransition: _popGestureInProgress.contains(route),
309
        child: _CupertinoBackGestureDetector<T>(
310 311
          enabledCallback: () => _isPopGestureEnabled<T>(route),
          onStartPopGesture: () => _startPopGesture<T>(route),
312 313
          child: child,
        ),
314
      );
315
    }
316
  }
317

318 319 320 321 322
  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child);
  }

323 324
  @override
  String get debugLabel => '${super.debugLabel}(${settings.name})';
325 326
}

327
/// Provides an iOS-style page transition animation.
328 329 330
///
/// 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.
331
class CupertinoPageTransition extends StatelessWidget {
332 333 334 335 336 337 338 339
  /// 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.
  ///  * `linearTransition` is whether to perform primary transition linearly.
  ///    Used to precisely track back gesture drags.
340 341
  CupertinoPageTransition({
    Key key,
342 343
    @required Animation<double> primaryRouteAnimation,
    @required Animation<double> secondaryRouteAnimation,
344
    @required this.child,
Ian Hickson's avatar
Ian Hickson committed
345 346
    @required bool linearTransition,
  }) : assert(linearTransition != null),
347
       _primaryPositionAnimation = (linearTransition ? primaryRouteAnimation :
348
         CurvedAnimation(
Ian Hickson's avatar
Ian Hickson committed
349 350
           parent: primaryRouteAnimation,
           curve: Curves.easeOut,
351
           reverseCurve: Curves.easeIn,
Ian Hickson's avatar
Ian Hickson committed
352
         )
353 354 355 356 357 358 359 360 361 362
       ).drive(_kRightMiddleTween),
       _secondaryPositionAnimation = CurvedAnimation(
         parent: secondaryRouteAnimation,
         curve: Curves.easeOut,
         reverseCurve: Curves.easeIn,
       ).drive(_kMiddleLeftTween),
       _primaryShadowAnimation = CurvedAnimation(
         parent: primaryRouteAnimation,
         curve: Curves.easeOut,
       ).drive(_kGradientShadowTween),
Ian Hickson's avatar
Ian Hickson committed
363
       super(key: key);
364 365

  // When this page is coming in to cover another page.
366
  final Animation<Offset> _primaryPositionAnimation;
367
  // When this page is becoming covered by another page.
368
  final Animation<Offset> _secondaryPositionAnimation;
369
  final Animation<Decoration> _primaryShadowAnimation;
370 371

  /// The widget below this widget in the tree.
372 373 374 375
  final Widget child;

  @override
  Widget build(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
376 377
    assert(debugCheckHasDirectionality(context));
    final TextDirection textDirection = Directionality.of(context);
378
    return SlideTransition(
379
      position: _secondaryPositionAnimation,
Ian Hickson's avatar
Ian Hickson committed
380
      textDirection: textDirection,
381
      transformHitTests: false,
382
      child: SlideTransition(
383
        position: _primaryPositionAnimation,
Ian Hickson's avatar
Ian Hickson committed
384
        textDirection: textDirection,
385
        child: DecoratedBoxTransition(
386
          decoration: _primaryShadowAnimation,
387 388 389
          child: child,
        ),
      ),
390 391 392 393
    );
  }
}

394 395 396 397
/// 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.
398
class CupertinoFullscreenDialogTransition extends StatelessWidget {
399
  /// Creates an iOS-style transition used for summoning fullscreen dialogs.
400 401 402 403
  CupertinoFullscreenDialogTransition({
    Key key,
    @required Animation<double> animation,
    @required this.child,
404 405 406
  }) : _positionAnimation = animation
         .drive(CurveTween(curve: Curves.easeInOut))
         .drive(_kBottomUpTween),
407
       super(key: key);
408

409
  final Animation<Offset> _positionAnimation;
410 411

  /// The widget below this widget in the tree.
412
  final Widget child;
413 414

  @override
415
  Widget build(BuildContext context) {
416
    return SlideTransition(
417
      position: _positionAnimation,
418 419
      child: child,
    );
420 421 422
  }
}

423 424 425 426 427
/// 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
428 429 430
///
/// The gesture data is converted from absolute coordinates to logical
/// coordinates by this widget.
431 432 433 434
///
/// The type `T` specifies the return type of the route with which this gesture
/// detector is associated.
class _CupertinoBackGestureDetector<T> extends StatefulWidget {
435 436 437 438 439 440 441 442 443 444 445 446 447 448
  const _CupertinoBackGestureDetector({
    Key key,
    @required this.enabledCallback,
    @required this.onStartPopGesture,
    @required this.child,
  }) : assert(enabledCallback != null),
       assert(onStartPopGesture != null),
       assert(child != null),
       super(key: key);

  final Widget child;

  final ValueGetter<bool> enabledCallback;

449
  final ValueGetter<_CupertinoBackGestureController<T>> onStartPopGesture;
450 451

  @override
452
  _CupertinoBackGestureDetectorState<T> createState() => _CupertinoBackGestureDetectorState<T>();
453 454
}

455 456
class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureDetector<T>> {
  _CupertinoBackGestureController<T> _backGestureController;
457 458 459 460 461 462

  HorizontalDragGestureRecognizer _recognizer;

  @override
  void initState() {
    super.initState();
463
    _recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
      ..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);
Ian Hickson's avatar
Ian Hickson committed
485
    _backGestureController.dragUpdate(_convertToLogical(details.primaryDelta / context.size.width));
486 487 488 489 490
  }

  void _handleDragEnd(DragEndDetails details) {
    assert(mounted);
    assert(_backGestureController != null);
Ian Hickson's avatar
Ian Hickson committed
491
    _backGestureController.dragEnd(_convertToLogical(details.velocity.pixelsPerSecond.dx / context.size.width));
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
    _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
508 509 510 511 512 513 514 515 516 517
  double _convertToLogical(double value) {
    switch (Directionality.of(context)) {
      case TextDirection.rtl:
        return -value;
      case TextDirection.ltr:
        return value;
    }
    return null;
  }

518 519
  @override
  Widget build(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
520
    assert(debugCheckHasDirectionality(context));
521
    return Stack(
522 523 524
      fit: StackFit.passthrough,
      children: <Widget>[
        widget.child,
525
        PositionedDirectional(
Ian Hickson's avatar
Ian Hickson committed
526
          start: 0.0,
527 528 529
          width: _kBackGestureWidth,
          top: 0.0,
          bottom: 0.0,
530
          child: Listener(
531 532 533
            onPointerDown: _handlePointerDown,
            behavior: HitTestBehavior.translucent,
          ),
534 535 536 537 538 539
        ),
      ],
    );
  }
}

540 541
/// A controller for an iOS-style back gesture.
///
542 543 544 545
/// 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
546 547 548
///
/// This class works entirely in logical coordinates (0.0 is new page dismissed,
/// 1.0 is new page on top).
549 550 551 552
///
/// The type `T` specifies the return type of the route with which this gesture
/// detector controller is associated.
class _CupertinoBackGestureController<T> {
553 554 555
  /// Creates a controller for an iOS-style back gesture.
  ///
  /// The [navigator] and [controller] arguments must not be null.
556 557
  _CupertinoBackGestureController({
    @required this.navigator,
558
    @required this.controller,
559 560 561 562 563 564 565 566 567
    @required this.onEnded,
  }) : assert(navigator != null),
       assert(controller != null),
       assert(onEnded != null) {
    navigator.didStartUserGesture();
  }

  /// The navigator that this object is controlling.
  final NavigatorState navigator;
568

569 570
  /// The animation controller that the route uses to drive its transition
  /// animation.
571 572
  final AnimationController controller;

573
  final VoidCallback onEnded;
574

575 576 577 578
  bool _animating = false;

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

583 584 585 586 587 588
  /// 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.
589 590 591 592 593 594 595
    if (velocity.abs() >= _kMinFlingVelocity) {
      controller.fling(velocity: -velocity);
    } else if (controller.value <= 0.5) {
      controller.fling(velocity: -1.0);
    } else {
      controller.fling(velocity: 1.0);
    }
596 597 598
    assert(controller.isAnimating);
    assert(controller.status != AnimationStatus.completed);
    assert(controller.status != AnimationStatus.dismissed);
599 600

    // Don't end the gesture until the transition completes.
601 602
    _animating = true;
    controller.addStatusListener(_handleStatusChanged);
603 604
  }

605
  void _handleStatusChanged(AnimationStatus status) {
606 607 608
    assert(_animating);
    controller.removeStatusListener(_handleStatusChanged);
    _animating = false;
609
    if (status == AnimationStatus.dismissed)
610
      navigator.pop<T>(); // this will cause the route to get disposed, which will dispose us
611 612 613 614 615 616 617
    onEnded(); // this will call dispose if popping the route failed to do so
  }

  void dispose() {
    if (_animating)
      controller.removeStatusListener(_handleStatusChanged);
    navigator.didStopUserGesture();
618 619
  }
}
620

Ian Hickson's avatar
Ian Hickson committed
621 622 623 624 625 626 627 628 629
// 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.
//
// The [edgeGradient] will be given a [TextDirection] when its shader is
// created, and so can be direction-sensitive; in this file we set it to a
// gradient that uses an AlignmentDirectional to position the gradient on the
// end edge of the gradient's box (which will be the edge adjacent to the start
// edge of the actual box we're supposed to paint in).
630 631 632
class _CupertinoEdgeShadowDecoration extends Decoration {
  const _CupertinoEdgeShadowDecoration({ this.edgeGradient });

633 634
  // An edge shadow decoration where the shadow is null. This is used
  // for interpolating from no shadow.
635
  static const _CupertinoEdgeShadowDecoration none =
636
      _CupertinoEdgeShadowDecoration();
637

638 639 640
  // A gradient to draw to the left of the box being decorated.
  // Alignments are relative to the original box translated one box
  // width to the left.
641 642
  final LinearGradient edgeGradient;

643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
  // 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].
660 661 662
  static _CupertinoEdgeShadowDecoration lerp(
    _CupertinoEdgeShadowDecoration a,
    _CupertinoEdgeShadowDecoration b,
663
    double t,
664
  ) {
665
    assert(t != null);
666 667
    if (a == null && b == null)
      return null;
668
    return _CupertinoEdgeShadowDecoration(
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688
      edgeGradient: LinearGradient.lerp(a?.edgeGradient, b?.edgeGradient, t),
    );
  }

  @override
  _CupertinoEdgeShadowDecoration lerpFrom(Decoration a, double t) {
    if (a is! _CupertinoEdgeShadowDecoration)
      return _CupertinoEdgeShadowDecoration.lerp(null, this, t);
    return _CupertinoEdgeShadowDecoration.lerp(a, this, t);
  }

  @override
  _CupertinoEdgeShadowDecoration lerpTo(Decoration b, double t) {
    if (b is! _CupertinoEdgeShadowDecoration)
      return _CupertinoEdgeShadowDecoration.lerp(this, null, t);
    return _CupertinoEdgeShadowDecoration.lerp(this, b, t);
  }

  @override
  _CupertinoEdgeShadowPainter createBoxPainter([VoidCallback onChanged]) {
689
    return _CupertinoEdgeShadowPainter(this, onChanged);
690 691 692 693
  }

  @override
  bool operator ==(dynamic other) {
Ian Hickson's avatar
Ian Hickson committed
694
    if (runtimeType != other.runtimeType)
695 696 697 698 699 700
      return false;
    final _CupertinoEdgeShadowDecoration typedOther = other;
    return edgeGradient == typedOther.edgeGradient;
  }

  @override
Ian Hickson's avatar
Ian Hickson committed
701
  int get hashCode => edgeGradient.hashCode;
702 703

  @override
704 705
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
706
    properties.add(DiagnosticsProperty<LinearGradient>('edgeGradient', edgeGradient));
707
  }
708 709 710 711 712 713
}

/// A [BoxPainter] used to draw the page transition shadow using gradients.
class _CupertinoEdgeShadowPainter extends BoxPainter {
  _CupertinoEdgeShadowPainter(
    this._decoration,
Ian Hickson's avatar
Ian Hickson committed
714
    VoidCallback onChange,
715 716 717 718 719 720 721 722 723 724 725
  ) : assert(_decoration != null),
      super(onChange);

  final _CupertinoEdgeShadowDecoration _decoration;

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    final LinearGradient gradient = _decoration.edgeGradient;
    if (gradient == null)
      return;
    // The drawable space for the gradient is a rect with the same size as
Ian Hickson's avatar
Ian Hickson committed
726 727 728 729 730 731 732 733 734 735 736 737 738
    // its parent box one box width on the start side of the box.
    final TextDirection textDirection = configuration.textDirection;
    assert(textDirection != null);
    double deltaX;
    switch (textDirection) {
      case TextDirection.rtl:
        deltaX = configuration.size.width;
        break;
      case TextDirection.ltr:
        deltaX = -configuration.size.width;
        break;
    }
    final Rect rect = (offset & configuration.size).translate(deltaX, 0.0);
739
    final Paint paint = Paint()
Ian Hickson's avatar
Ian Hickson committed
740
      ..shader = gradient.createShader(rect, textDirection: textDirection);
741 742 743 744

    canvas.drawRect(rect, paint);
  }
}
745

746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
class _CupertinoModalPopupRoute<T> extends PopupRoute<T> {
  _CupertinoModalPopupRoute({
    this.builder,
    this.barrierLabel,
    RouteSettings settings,
  }) : super(settings: settings);

  final WidgetBuilder builder;

  @override
  final String barrierLabel;

  @override
  Color get barrierColor => _kModalBarrierColor;

  @override
  bool get barrierDismissible => true;

  @override
  bool get semanticsDismissible => false;

  @override
  Duration get transitionDuration => _kModalPopupTransitionDuration;

  Animation<double> _animation;

  Tween<Offset> _offsetTween;

  @override
  Animation<double> createAnimation() {
    assert(_animation == null);
777
    _animation = CurvedAnimation(
778 779 780 781
      parent: super.createAnimation(),
      curve: Curves.ease,
      reverseCurve: Curves.ease.flipped,
    );
782
    _offsetTween = Tween<Offset>(
783 784 785 786 787 788 789 790 791 792 793 794 795
      begin: const Offset(0.0, 1.0),
      end: const Offset(0.0, 0.0),
    );
    return _animation;
  }

  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    return builder(context);
  }

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
796
    return Align(
797
      alignment: Alignment.bottomCenter,
798
      child: FractionalTranslation(
799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834
        translation: _offsetTween.evaluate(_animation),
        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.
///
/// 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.
///
/// See also:
///
///  * [ActionSheet], which is the widget usually returned by the `builder`
///    argument to [showCupertinoModalPopup].
///  * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
Future<T> showCupertinoModalPopup<T>({
  @required BuildContext context,
  @required WidgetBuilder builder,
}) {
  return Navigator.of(context, rootNavigator: true).push(
835
    _CupertinoModalPopupRoute<T>(
836 837 838 839 840 841
      builder: builder,
      barrierLabel: 'Dismiss',
    ),
  );
}

842 843 844
final Animatable<double> _dialogTween = Tween<double>(begin: 1.2, end: 1.0)
  .chain(CurveTween(curve: Curves.fastOutSlowIn));

845
Widget _buildCupertinoDialogTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
846
  final CurvedAnimation fadeAnimation = CurvedAnimation(
847 848 849 850
    parent: animation,
    curve: Curves.easeInOut,
  );
  if (animation.status == AnimationStatus.reverse) {
851
    return FadeTransition(
852 853 854 855
      opacity: fadeAnimation,
      child: child,
    );
  }
856
  return FadeTransition(
857 858 859
    opacity: fadeAnimation,
    child: ScaleTransition(
      child: child,
860
      scale: animation.drive(_dialogTween),
861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908
    ),
  );
}

/// Displays an iOS-style dialog above the current contents of the app, with
/// iOS-style entrance and exit animations, modal barrier color, and modal
/// barrier behavior (the dialog is not dismissible with a tap on the barrier).
///
/// This function takes a `builder` which typically builds a [CupertinoDialog]
/// or [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.
///
/// 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.
///
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
/// The dialog route created by this method is pushed to the root navigator.
/// 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)`.
///
/// See also:
///  * [CupertinoDialog], an iOS-style dialog.
///  * [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/>
Future<T> showCupertinoDialog<T>({
  @required BuildContext context,
  @required WidgetBuilder builder,
}) {
  assert(builder != null);
  return showGeneralDialog(
    context: context,
    barrierDismissible: false,
    barrierColor: _kModalBarrierColor,
    transitionDuration: const Duration(milliseconds: 300),
    pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
      return builder(context);
    },
    transitionBuilder: _buildCupertinoDialogTransitions,
  );
909
}