route.dart 23.7 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], [settings], [maintainState], and [fullscreenDialog]
  /// arguments must not be null.
81 82 83 84 85
  CupertinoPageRoute({
    @required this.builder,
    RouteSettings settings: const RouteSettings(),
    this.maintainState: true,
    bool fullscreenDialog: false,
86
    this.hostRoute,
87
  }) : assert(builder != null),
88 89 90
       assert(settings != null),
       assert(maintainState != null),
       assert(fullscreenDialog != null),
91
       super(settings: settings, fullscreenDialog: fullscreenDialog) {
92
    // ignore: prefer_asserts_in_initializer_lists , https://github.com/dart-lang/sdk/issues/31223
93 94
    assert(opaque); // PageRoute makes it return true.
  }
95 96 97

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

99 100
  @override
  final bool maintainState;
101

102 103 104 105 106 107 108 109 110 111
  /// 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;

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

  @override
116
  Color get barrierColor => null;
117

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

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

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

132 133 134 135 136 137 138 139 140
  @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).'
      );
141
    }());
142 143 144
    super.install(insertionPoint);
  }

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

152
  _CupertinoBackGestureController _backGestureController;
153

154
  /// Whether a pop gesture is currently underway.
155
  ///
156 157 158
  /// 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.
159 160 161
  ///
  /// See also:
  ///
162 163 164
  ///  * [popGestureEnabled], which returns whether a pop gesture is appropriate
  ///    in the first place.
  bool get popGestureInProgress => _backGestureController != null;
165

166
  /// Whether a pop gesture can be started by the user.
167 168 169 170
  ///
  /// This returns true if the user can edge-swipe to a previous route,
  /// otherwise false.
  ///
171 172 173
  /// This will return false once [popGestureInProgress] is true, but
  /// [popGestureInProgress] can only become true if [popGestureEnabled] was
  /// true first.
174
  ///
175 176 177 178 179 180 181 182 183 184 185
  /// 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;
186 187
    // 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.
188 189
    if (route.hasScopedWillPopCallback)
      return false;
190 191
    // Fullscreen dialogs aren't dismissable by back swipe.
    if (fullscreenDialog)
192 193 194 195 196 197 198 199 200 201
      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;
  }
202

203 204 205
  /// Begin dismissing this route from a horizontal swipe, if appropriate.
  ///
  /// Swiping will be disabled if the page is a fullscreen dialog or if
206
  /// dismissals can be overridden because a [WillPopCallback] was
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
  /// 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.
  _CupertinoBackGestureController _startPopGesture() {
    assert(!popGestureInProgress);
    assert(popGestureEnabled);
    final PageRoute<T> route = hostRoute ?? this;
    _backGestureController = new _CupertinoBackGestureController(
      navigator: route.navigator,
      controller: route.controller,
      onEnded: _endPopGesture,
    );
229 230
    return _backGestureController;
  }
231

232 233 234 235 236 237 238
  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;
  }

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

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

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

281
/// Provides an iOS-style page transition animation.
282 283 284
///
/// 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.
285
class CupertinoPageTransition extends StatelessWidget {
286 287 288 289 290 291 292 293
  /// 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.
294 295
  CupertinoPageTransition({
    Key key,
296 297
    @required Animation<double> primaryRouteAnimation,
    @required Animation<double> secondaryRouteAnimation,
298
    @required this.child,
Ian Hickson's avatar
Ian Hickson committed
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
    @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);
324 325

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

  /// The widget below this widget in the tree.
332 333 334 335
  final Widget child;

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

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

373
  final Animation<Offset> _positionAnimation;
374 375

  /// The widget below this widget in the tree.
376
  final Widget child;
377 378

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

387 388 389 390 391
/// 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
392 393 394
///
/// The gesture data is converted from absolute coordinates to logical
/// coordinates by this widget.
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
class _CupertinoBackGestureDetector extends StatefulWidget {
  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;

  final ValueGetter<_CupertinoBackGestureController> onStartPopGesture;

  @override
  _CupertinoBackGestureDetectorState createState() => new _CupertinoBackGestureDetectorState();
}

class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureDetector> {
  _CupertinoBackGestureController _backGestureController;

  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
446
    _backGestureController.dragUpdate(_convertToLogical(details.primaryDelta / context.size.width));
447 448 449 450 451
  }

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

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


502 503
/// A controller for an iOS-style back gesture.
///
504 505 506 507
/// 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
508 509 510
///
/// This class works entirely in logical coordinates (0.0 is new page dismissed,
/// 1.0 is new page on top).
511
class _CupertinoBackGestureController {
512 513 514
  /// Creates a controller for an iOS-style back gesture.
  ///
  /// The [navigator] and [controller] arguments must not be null.
515 516
  _CupertinoBackGestureController({
    @required this.navigator,
517
    @required this.controller,
518 519 520 521 522 523 524 525 526
    @required this.onEnded,
  }) : assert(navigator != null),
       assert(controller != null),
       assert(onEnded != null) {
    navigator.didStartUserGesture();
  }

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

528 529
  /// The animation controller that the route uses to drive its transition
  /// animation.
530 531
  final AnimationController controller;

532
  final VoidCallback onEnded;
533

534 535 536 537
  bool _animating = false;

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

542 543 544 545 546 547
  /// 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.
548 549 550 551 552 553 554
    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);
    }
555 556 557
    assert(controller.isAnimating);
    assert(controller.status != AnimationStatus.completed);
    assert(controller.status != AnimationStatus.dismissed);
558 559

    // Don't end the gesture until the transition completes.
560 561
    _animating = true;
    controller.addStatusListener(_handleStatusChanged);
562 563
  }

564
  void _handleStatusChanged(AnimationStatus status) {
565 566 567
    assert(_animating);
    controller.removeStatusListener(_handleStatusChanged);
    _animating = false;
568
    if (status == AnimationStatus.dismissed)
569 570 571 572 573 574 575 576
      navigator.pop(); // this will cause the route to get disposed, which will dispose us
    onEnded(); // this will call dispose if popping the route failed to do so
  }

  void dispose() {
    if (_animating)
      controller.removeStatusListener(_handleStatusChanged);
    navigator.didStopUserGesture();
577 578
  }
}
579

Ian Hickson's avatar
Ian Hickson committed
580 581 582 583 584 585 586 587 588
// 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).
589 590 591
class _CupertinoEdgeShadowDecoration extends Decoration {
  const _CupertinoEdgeShadowDecoration({ this.edgeGradient });

592 593
  // An edge shadow decoration where the shadow is null. This is used
  // for interpolating from no shadow.
594 595 596
  static const _CupertinoEdgeShadowDecoration none =
      const _CupertinoEdgeShadowDecoration();

597 598 599
  // 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.
600 601
  final LinearGradient edgeGradient;

602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
  // 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].
619 620 621
  static _CupertinoEdgeShadowDecoration lerp(
    _CupertinoEdgeShadowDecoration a,
    _CupertinoEdgeShadowDecoration b,
622
    double t,
623
  ) {
624
    assert(t != null);
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
    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
653
    if (runtimeType != other.runtimeType)
654 655 656 657 658 659
      return false;
    final _CupertinoEdgeShadowDecoration typedOther = other;
    return edgeGradient == typedOther.edgeGradient;
  }

  @override
Ian Hickson's avatar
Ian Hickson committed
660
  int get hashCode => edgeGradient.hashCode;
661 662

  @override
663 664 665
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new DiagnosticsProperty<LinearGradient>('edgeGradient', edgeGradient));
666
  }
667 668 669 670 671 672
}

/// 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
673
    VoidCallback onChange,
674 675 676 677 678 679 680 681 682 683 684
  ) : 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
685 686 687 688 689 690 691 692 693 694 695 696 697
    // 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);
698
    final Paint paint = new Paint()
Ian Hickson's avatar
Ian Hickson committed
699
      ..shader = gradient.createShader(rect, textDirection: textDirection);
700 701 702 703

    canvas.drawRect(rect, paint);
  }
}