route.dart 24 KB
Newer Older
1 2 3 4 5
// 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.

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

10 11
const double _kBackGestureWidth = 20.0;
const double _kMinFlingVelocity = 1.0; // Screen widths per second.
12

13 14 15 16
// Offset from offscreen to the right to fully on screen.
final Tween<Offset> _kRightMiddleTween = new Tween<Offset>(
  begin: const Offset(1.0, 0.0),
  end: Offset.zero,
17 18
);

19 20 21 22
// Offset from fully on screen to 1/3 offscreen to the left.
final Tween<Offset> _kMiddleLeftTween = new Tween<Offset>(
  begin: Offset.zero,
  end: const Offset(-1.0/3.0, 0.0),
23 24
);

25 26 27 28
// Offset from offscreen below to fully on screen.
final Tween<Offset> _kBottomUpTween = new Tween<Offset>(
  begin: const Offset(0.0, 1.0),
  end: Offset.zero,
29 30
);

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

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

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

98 99
  @override
  final bool maintainState;
100

101 102 103 104 105 106 107 108 109 110
  /// The route that owns this one.
  ///
  /// The [MaterialPageRoute] creates a [CupertinoPageRoute] to handle iOS-style
  /// navigation. When this happens, the [MaterialPageRoute] is the [hostRoute]
  /// of this [CupertinoPageRoute].
  ///
  /// The [hostRoute] is responsible for calling [dispose] on the route. When
  /// there is a [hostRoute], the [CupertinoPageRoute] must not be [install]ed.
  final PageRoute<T> hostRoute;

111 112
  @override
  Duration get transitionDuration => const Duration(milliseconds: 350);
113 114

  @override
115
  Color get barrierColor => null;
116

117 118 119
  @override
  String get barrierLabel => null;

120
  @override
121 122
  bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) {
    return previousRoute is CupertinoPageRoute;
123
  }
124

125
  @override
126 127 128
  bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
    // Don't perform outgoing animation if the next route is a fullscreen dialog.
    return nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog;
129 130
  }

131 132 133 134 135 136 137 138 139
  @override
  void install(OverlayEntry insertionPoint) {
    assert(() {
      if (hostRoute == null)
        return true;
      throw new FlutterError(
        'Cannot install a subsidiary route (one with a hostRoute).\n'
        'This route ($this) cannot be installed, because it has a host route ($hostRoute).'
      );
140
    }());
141 142 143
    super.install(insertionPoint);
  }

144
  @override
145 146
  void dispose() {
    _backGestureController?.dispose();
147 148
    _backGestureController = null;
    super.dispose();
149 150
  }

151
  _CupertinoBackGestureController<T> _backGestureController;
152

153
  /// Whether a pop gesture is currently underway.
154
  ///
155 156 157
  /// This starts returning true when pop gesture is started by the user. It
  /// returns false if that has not yet occurred or if the most recent such
  /// gesture has completed.
158 159 160
  ///
  /// See also:
  ///
161 162 163
  ///  * [popGestureEnabled], which returns whether a pop gesture is appropriate
  ///    in the first place.
  bool get popGestureInProgress => _backGestureController != null;
164

165
  /// Whether a pop gesture can be started by the user.
166 167 168 169
  ///
  /// This returns true if the user can edge-swipe to a previous route,
  /// otherwise false.
  ///
170 171 172
  /// This will return false once [popGestureInProgress] is true, but
  /// [popGestureInProgress] can only become true if [popGestureEnabled] was
  /// true first.
173
  ///
174 175 176 177 178 179 180 181 182 183 184
  /// This should only be used between frames, not during build.
  bool get popGestureEnabled {
    final PageRoute<T> route = hostRoute ?? this;
    // 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;
185 186
    // 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.
187 188
    if (route.hasScopedWillPopCallback)
      return false;
189 190
    // Fullscreen dialogs aren't dismissable by back swipe.
    if (fullscreenDialog)
191 192 193 194 195 196 197 198 199 200
      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.
    if (popGestureInProgress)
      return false;
    // Looks like a back gesture would be welcome!
    return true;
  }
201

202 203 204
  /// Begin dismissing this route from a horizontal swipe, if appropriate.
  ///
  /// Swiping will be disabled if the page is a fullscreen dialog or if
205
  /// dismissals can be overridden because a [WillPopCallback] was
206 207 208 209 210 211 212 213 214 215 216 217 218
  /// defined for the route.
  ///
  /// When this method decides a pop gesture is appropriate, it returns a
  /// [CupertinoBackGestureController].
  ///
  /// See also:
  ///
  ///  * [hasScopedWillPopCallback], which is true if a `willPop` callback
  ///    is defined for this route.
  ///  * [popGestureEnabled], which returns whether a pop gesture is
  ///    appropriate.
  ///  * [Route.startPopGesture], which describes the contract that this method
  ///    must implement.
219
  _CupertinoBackGestureController<T> _startPopGesture() {
220 221 222
    assert(!popGestureInProgress);
    assert(popGestureEnabled);
    final PageRoute<T> route = hostRoute ?? this;
223
    _backGestureController = new _CupertinoBackGestureController<T>(
224 225 226 227
      navigator: route.navigator,
      controller: route.controller,
      onEnded: _endPopGesture,
    );
228 229
    return _backGestureController;
  }
230

231 232 233 234 235 236 237
  void _endPopGesture() {
    // In practice this only gets called if for some reason popping the route
    // did not cause this route to get disposed.
    _backGestureController?.dispose();
    _backGestureController = null;
  }

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

257 258
  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
259
    if (fullscreenDialog) {
260 261 262 263
      return new CupertinoFullscreenDialogTransition(
        animation: animation,
        child: child,
      );
264
    } else {
265 266 267
      return new CupertinoPageTransition(
        primaryRouteAnimation: animation,
        secondaryRouteAnimation: secondaryAnimation,
268 269 270
        // In the middle of a back gesture drag, let the transition be linear to
        // match finger motions.
        linearTransition: popGestureInProgress,
271
        child: new _CupertinoBackGestureDetector<T>(
272 273 274 275
          enabledCallback: () => popGestureEnabled,
          onStartPopGesture: _startPopGesture,
          child: child,
        ),
276
      );
277
    }
278
  }
279 280 281

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

284
/// Provides an iOS-style page transition animation.
285 286 287
///
/// 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.
288
class CupertinoPageTransition extends StatelessWidget {
289 290 291 292 293 294 295 296
  /// 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.
297 298
  CupertinoPageTransition({
    Key key,
299 300
    @required Animation<double> primaryRouteAnimation,
    @required Animation<double> secondaryRouteAnimation,
301
    @required this.child,
Ian Hickson's avatar
Ian Hickson committed
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
    @required bool linearTransition,
  }) : assert(linearTransition != null),
       _primaryPositionAnimation = linearTransition
         ? _kRightMiddleTween.animate(primaryRouteAnimation)
         : _kRightMiddleTween.animate(
             new CurvedAnimation(
               parent: primaryRouteAnimation,
               curve: Curves.easeOut,
               reverseCurve: Curves.easeIn,
             )
           ),
       _secondaryPositionAnimation = _kMiddleLeftTween.animate(
         new CurvedAnimation(
           parent: secondaryRouteAnimation,
           curve: Curves.easeOut,
           reverseCurve: Curves.easeIn,
         )
       ),
       _primaryShadowAnimation = _kGradientShadowTween.animate(
         new CurvedAnimation(
           parent: primaryRouteAnimation,
           curve: Curves.easeOut,
         )
       ),
       super(key: key);
327 328

  // When this page is coming in to cover another page.
329
  final Animation<Offset> _primaryPositionAnimation;
330
  // When this page is becoming covered by another page.
331
  final Animation<Offset> _secondaryPositionAnimation;
332
  final Animation<Decoration> _primaryShadowAnimation;
333 334

  /// The widget below this widget in the tree.
335 336 337 338
  final Widget child;

  @override
  Widget build(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
339 340
    assert(debugCheckHasDirectionality(context));
    final TextDirection textDirection = Directionality.of(context);
341 342 343
    // TODO(ianh): tell the transform to be un-transformed for hit testing
    // but not while being controlled by a gesture.
    return new SlideTransition(
344
      position: _secondaryPositionAnimation,
Ian Hickson's avatar
Ian Hickson committed
345
      textDirection: textDirection,
346
      child: new SlideTransition(
347
        position: _primaryPositionAnimation,
Ian Hickson's avatar
Ian Hickson committed
348
        textDirection: textDirection,
349 350
        child: new DecoratedBoxTransition(
          decoration: _primaryShadowAnimation,
351 352 353
          child: child,
        ),
      ),
354 355 356 357
    );
  }
}

358 359 360 361
/// 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.
362
class CupertinoFullscreenDialogTransition extends StatelessWidget {
363
  /// Creates an iOS-style transition used for summoning fullscreen dialogs.
364 365 366 367
  CupertinoFullscreenDialogTransition({
    Key key,
    @required Animation<double> animation,
    @required this.child,
368 369 370 371 372 373 374
  }) : _positionAnimation = _kBottomUpTween.animate(
         new CurvedAnimation(
           parent: animation,
           curve: Curves.easeInOut,
         )
       ),
       super(key: key);
375

376
  final Animation<Offset> _positionAnimation;
377 378

  /// The widget below this widget in the tree.
379
  final Widget child;
380 381

  @override
382 383
  Widget build(BuildContext context) {
    return new SlideTransition(
384
      position: _positionAnimation,
385 386
      child: child,
    );
387 388 389
  }
}

390 391 392 393 394
/// 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
395 396 397
///
/// The gesture data is converted from absolute coordinates to logical
/// coordinates by this widget.
398 399 400 401
///
/// The type `T` specifies the return type of the route with which this gesture
/// detector is associated.
class _CupertinoBackGestureDetector<T> extends StatefulWidget {
402 403 404 405 406 407 408 409 410 411 412 413 414 415
  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;

416
  final ValueGetter<_CupertinoBackGestureController<T>> onStartPopGesture;
417 418

  @override
419
  _CupertinoBackGestureDetectorState<T> createState() => new _CupertinoBackGestureDetectorState<T>();
420 421
}

422 423
class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureDetector<T>> {
  _CupertinoBackGestureController<T> _backGestureController;
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451

  HorizontalDragGestureRecognizer _recognizer;

  @override
  void initState() {
    super.initState();
    _recognizer = new HorizontalDragGestureRecognizer(debugOwner: this)
      ..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
452
    _backGestureController.dragUpdate(_convertToLogical(details.primaryDelta / context.size.width));
453 454 455 456 457
  }

  void _handleDragEnd(DragEndDetails details) {
    assert(mounted);
    assert(_backGestureController != null);
Ian Hickson's avatar
Ian Hickson committed
458
    _backGestureController.dragEnd(_convertToLogical(details.velocity.pixelsPerSecond.dx / context.size.width));
459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
    _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
475 476 477 478 479 480 481 482 483 484
  double _convertToLogical(double value) {
    switch (Directionality.of(context)) {
      case TextDirection.rtl:
        return -value;
      case TextDirection.ltr:
        return value;
    }
    return null;
  }

485 486
  @override
  Widget build(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
487
    assert(debugCheckHasDirectionality(context));
488 489 490 491
    return new Stack(
      fit: StackFit.passthrough,
      children: <Widget>[
        widget.child,
Ian Hickson's avatar
Ian Hickson committed
492 493
        new PositionedDirectional(
          start: 0.0,
494 495 496 497 498 499 500
          width: _kBackGestureWidth,
          top: 0.0,
          bottom: 0.0,
          child: new Listener(
            onPointerDown: _handlePointerDown,
            behavior: HitTestBehavior.translucent,
          ),
501 502 503 504 505 506 507
        ),
      ],
    );
  }
}


508 509
/// A controller for an iOS-style back gesture.
///
510 511 512 513
/// 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
514 515 516
///
/// This class works entirely in logical coordinates (0.0 is new page dismissed,
/// 1.0 is new page on top).
517 518 519 520
///
/// The type `T` specifies the return type of the route with which this gesture
/// detector controller is associated.
class _CupertinoBackGestureController<T> {
521 522 523
  /// Creates a controller for an iOS-style back gesture.
  ///
  /// The [navigator] and [controller] arguments must not be null.
524 525
  _CupertinoBackGestureController({
    @required this.navigator,
526
    @required this.controller,
527 528 529 530 531 532 533 534 535
    @required this.onEnded,
  }) : assert(navigator != null),
       assert(controller != null),
       assert(onEnded != null) {
    navigator.didStartUserGesture();
  }

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

537 538
  /// The animation controller that the route uses to drive its transition
  /// animation.
539 540
  final AnimationController controller;

541
  final VoidCallback onEnded;
542

543 544 545 546
  bool _animating = false;

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

551 552 553 554 555 556
  /// 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.
557 558 559 560 561 562 563
    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);
    }
564 565 566
    assert(controller.isAnimating);
    assert(controller.status != AnimationStatus.completed);
    assert(controller.status != AnimationStatus.dismissed);
567 568

    // Don't end the gesture until the transition completes.
569 570
    _animating = true;
    controller.addStatusListener(_handleStatusChanged);
571 572
  }

573
  void _handleStatusChanged(AnimationStatus status) {
574 575 576
    assert(_animating);
    controller.removeStatusListener(_handleStatusChanged);
    _animating = false;
577
    if (status == AnimationStatus.dismissed)
578
      navigator.pop<T>(); // this will cause the route to get disposed, which will dispose us
579 580 581 582 583 584 585
    onEnded(); // this will call dispose if popping the route failed to do so
  }

  void dispose() {
    if (_animating)
      controller.removeStatusListener(_handleStatusChanged);
    navigator.didStopUserGesture();
586 587
  }
}
588

Ian Hickson's avatar
Ian Hickson committed
589 590 591 592 593 594 595 596 597
// 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).
598 599 600
class _CupertinoEdgeShadowDecoration extends Decoration {
  const _CupertinoEdgeShadowDecoration({ this.edgeGradient });

601 602
  // An edge shadow decoration where the shadow is null. This is used
  // for interpolating from no shadow.
603 604 605
  static const _CupertinoEdgeShadowDecoration none =
      const _CupertinoEdgeShadowDecoration();

606 607 608
  // 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.
609 610
  final LinearGradient edgeGradient;

611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627
  // 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].
628 629 630
  static _CupertinoEdgeShadowDecoration lerp(
    _CupertinoEdgeShadowDecoration a,
    _CupertinoEdgeShadowDecoration b,
631
    double t,
632
  ) {
633
    assert(t != null);
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
    if (a == null && b == null)
      return null;
    return new _CupertinoEdgeShadowDecoration(
      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]) {
    return new _CupertinoEdgeShadowPainter(this, onChanged);
  }

  @override
  bool operator ==(dynamic other) {
Ian Hickson's avatar
Ian Hickson committed
662
    if (runtimeType != other.runtimeType)
663 664 665 666 667 668
      return false;
    final _CupertinoEdgeShadowDecoration typedOther = other;
    return edgeGradient == typedOther.edgeGradient;
  }

  @override
Ian Hickson's avatar
Ian Hickson committed
669
  int get hashCode => edgeGradient.hashCode;
670 671

  @override
672 673 674
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new DiagnosticsProperty<LinearGradient>('edgeGradient', edgeGradient));
675
  }
676 677 678 679 680 681
}

/// 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
682
    VoidCallback onChange,
683 684 685 686 687 688 689 690 691 692 693
  ) : 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
694 695 696 697 698 699 700 701 702 703 704 705 706
    // 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);
707
    final Paint paint = new Paint()
Ian Hickson's avatar
Ian Hickson committed
708
      ..shader = gradient.createShader(rect, textDirection: textDirection);
709 710 711 712

    canvas.drawRect(rect, paint);
  }
}