| // Copyright 2014 The Flutter 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 'dart:async'; |
| import 'dart:math'; |
| import 'dart:ui' show lerpDouble, ImageFilter; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter/animation.dart' show Curves; |
| |
| import 'colors.dart'; |
| import 'interface_level.dart'; |
| |
| const double _kBackGestureWidth = 20.0; |
| const double _kMinFlingVelocity = 1.0; // Screen widths per second. |
| |
| // 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. |
| |
| // Barrier color for a Cupertino modal barrier. |
| // Extracted from https://developer.apple.com/design/resources/. |
| const Color _kModalBarrierColor = CupertinoDynamicColor.withBrightness( |
| color: Color(0x33000000), |
| darkColor: Color(0x7A000000), |
| ); |
| |
| // The duration of the transition used when a modal popup is shown. |
| const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335); |
| |
| // Offset from offscreen to the right to fully on screen. |
| final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( |
| begin: const Offset(1.0, 0.0), |
| end: Offset.zero, |
| ); |
| |
| // Offset from fully on screen to 1/3 offscreen to the left. |
| final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>( |
| begin: Offset.zero, |
| end: const Offset(-1.0/3.0, 0.0), |
| ); |
| |
| // Offset from offscreen below to fully on screen. |
| final Animatable<Offset> _kBottomUpTween = Tween<Offset>( |
| begin: const Offset(0.0, 1.0), |
| end: Offset.zero, |
| ); |
| |
| // Custom decoration from no shadow to page shadow mimicking iOS page |
| // transitions using gradients. |
| final DecorationTween _kGradientShadowTween = DecorationTween( |
| begin: _CupertinoEdgeShadowDecoration.none, // No decoration initially. |
| end: const _CupertinoEdgeShadowDecoration( |
| edgeGradient: LinearGradient( |
| // Spans 5% of the page. |
| begin: AlignmentDirectional(0.90, 0.0), |
| end: AlignmentDirectional.centerEnd, |
| // Eyeballed gradient used to mimic a drop shadow on the start side only. |
| colors: <Color>[ |
| Color(0x00000000), |
| Color(0x04000000), |
| Color(0x12000000), |
| Color(0x38000000), |
| ], |
| stops: <double>[0.0, 0.3, 0.6, 1.0], |
| ), |
| ), |
| ); |
| |
| /// A modal route that replaces the entire screen with an iOS transition. |
| /// |
| /// 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. |
| /// |
| /// The page slides in from the bottom and exits in reverse with no parallax |
| /// effect for fullscreen dialogs. |
| /// |
| /// 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: |
| /// |
| /// * [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. |
| class CupertinoPageRoute<T> extends PageRoute<T> { |
| /// Creates a page route for use in an iOS designed app. |
| /// |
| /// The [builder], [maintainState], and [fullscreenDialog] arguments must not |
| /// be null. |
| CupertinoPageRoute({ |
| @required this.builder, |
| this.title, |
| RouteSettings settings, |
| this.maintainState = true, |
| bool fullscreenDialog = false, |
| }) : assert(builder != null), |
| assert(maintainState != null), |
| assert(fullscreenDialog != null), |
| assert(opaque), |
| super(settings: settings, fullscreenDialog: fullscreenDialog); |
| |
| /// Builds the primary contents of the route. |
| final WidgetBuilder builder; |
| |
| /// A title string for this route. |
| /// |
| /// Used to auto-populate [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) { |
| _previousTitle = ValueNotifier<String>(previousTitleString); |
| } else { |
| _previousTitle.value = previousTitleString; |
| } |
| super.didChangePrevious(previousRoute); |
| } |
| |
| @override |
| final bool maintainState; |
| |
| @override |
| // A relatively rigorous eyeball estimation. |
| Duration get transitionDuration => const Duration(milliseconds: 400); |
| |
| @override |
| Color get barrierColor => null; |
| |
| @override |
| String get barrierLabel => null; |
| |
| @override |
| bool canTransitionTo(TransitionRoute<dynamic> nextRoute) { |
| // Don't perform outgoing animation if the next route is a fullscreen dialog. |
| return nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog; |
| } |
| |
| /// True if an iOS-style back swipe pop gesture is currently underway for [route]. |
| /// |
| /// This just check the route's [NavigatorState.userGestureInProgress]. |
| /// |
| /// See also: |
| /// |
| /// * [popGestureEnabled], which returns true if a user-triggered pop gesture |
| /// would be allowed. |
| static bool isPopGestureInProgress(PageRoute<dynamic> route) { |
| return route.navigator.userGestureInProgress; |
| } |
| |
| /// True if an iOS-style back swipe pop gesture is currently underway for this route. |
| /// |
| /// See also: |
| /// |
| /// * [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); |
| |
| /// Whether a pop gesture can be started by the user. |
| /// |
| /// Returns true if the user can edge-swipe to a previous route. |
| /// |
| /// Returns false once [isPopGestureInProgress] is true, but |
| /// [isPopGestureInProgress] can only become true if [popGestureEnabled] was |
| /// true first. |
| /// |
| /// This should only be used between frames, not during build. |
| bool get popGestureEnabled => _isPopGestureEnabled(this); |
| |
| static bool _isPopGestureEnabled<T>(PageRoute<T> route) { |
| // 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; |
| // 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. |
| if (route.hasScopedWillPopCallback) |
| return false; |
| // Fullscreen dialogs aren't dismissible by back swipe. |
| if (route.fullscreenDialog) |
| return false; |
| // If we're in an animation already, we cannot be manually swiped. |
| if (route.animation.status != AnimationStatus.completed) |
| 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. |
| if (route.secondaryAnimation.status != AnimationStatus.dismissed) |
| return false; |
| // If we're in a gesture already, we cannot start another. |
| if (isPopGestureInProgress(route)) |
| return false; |
| |
| // Looks like a back gesture would be welcome! |
| return true; |
| } |
| |
| @override |
| Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| final Widget child = builder(context); |
| final Widget result = Semantics( |
| scopesRoute: true, |
| explicitChildNodes: true, |
| child: child, |
| ); |
| assert(() { |
| if (child == null) { |
| throw FlutterError.fromParts(<DiagnosticsNode>[ |
| ErrorSummary('The builder for route "${settings.name}" returned null.'), |
| ErrorDescription('Route builders must never return null.'), |
| ]); |
| } |
| return true; |
| }()); |
| return result; |
| } |
| |
| // Called by _CupertinoBackGestureDetector when a pop ("back") drag start |
| // gesture is detected. The returned controller handles all of the subsequent |
| // drag events. |
| static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) { |
| assert(_isPopGestureEnabled(route)); |
| |
| return _CupertinoBackGestureController<T>( |
| navigator: route.navigator, |
| controller: route.controller, // protected access |
| ); |
| } |
| |
| /// 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, |
| ) { |
| // 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); |
| if (route.fullscreenDialog) { |
| return CupertinoFullscreenDialogTransition( |
| primaryRouteAnimation: animation, |
| secondaryRouteAnimation: secondaryAnimation, |
| child: child, |
| linearTransition: linearTransition, |
| ); |
| } else { |
| return CupertinoPageTransition( |
| primaryRouteAnimation: animation, |
| secondaryRouteAnimation: secondaryAnimation, |
| linearTransition: linearTransition, |
| child: _CupertinoBackGestureDetector<T>( |
| enabledCallback: () => _isPopGestureEnabled<T>(route), |
| onStartPopGesture: () => _startPopGesture<T>(route), |
| child: child, |
| ), |
| ); |
| } |
| } |
| |
| @override |
| Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { |
| return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child); |
| } |
| |
| @override |
| String get debugLabel => '${super.debugLabel}(${settings.name})'; |
| } |
| |
| /// Provides an iOS-style page transition animation. |
| /// |
| /// 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. |
| class CupertinoPageTransition extends StatelessWidget { |
| /// 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 the transitions linearly. |
| /// Used to precisely track back gesture drags. |
| CupertinoPageTransition({ |
| Key key, |
| @required Animation<double> primaryRouteAnimation, |
| @required Animation<double> secondaryRouteAnimation, |
| @required this.child, |
| @required bool linearTransition, |
| }) : assert(linearTransition != null), |
| _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, |
| ) |
| ).drive(_kGradientShadowTween), |
| super(key: key); |
| |
| // When this page is coming in to cover another page. |
| final Animation<Offset> _primaryPositionAnimation; |
| // When this page is becoming covered by another page. |
| final Animation<Offset> _secondaryPositionAnimation; |
| final Animation<Decoration> _primaryShadowAnimation; |
| |
| /// The widget below this widget in the tree. |
| final Widget child; |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasDirectionality(context)); |
| final TextDirection textDirection = Directionality.of(context); |
| return SlideTransition( |
| position: _secondaryPositionAnimation, |
| textDirection: textDirection, |
| transformHitTests: false, |
| child: SlideTransition( |
| position: _primaryPositionAnimation, |
| textDirection: textDirection, |
| child: DecoratedBoxTransition( |
| decoration: _primaryShadowAnimation, |
| child: child, |
| ), |
| ), |
| ); |
| } |
| } |
| |
| /// 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. |
| class CupertinoFullscreenDialogTransition extends StatelessWidget { |
| /// Creates an iOS-style transition used for summoning fullscreen dialogs. |
| /// |
| /// * `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. |
| CupertinoFullscreenDialogTransition({ |
| Key key, |
| @required Animation<double> primaryRouteAnimation, |
| @required Animation<double> secondaryRouteAnimation, |
| @required this.child, |
| @required bool linearTransition, |
| }) : _positionAnimation = CurvedAnimation( |
| parent: primaryRouteAnimation, |
| 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), |
| _secondaryPositionAnimation = |
| (linearTransition |
| ? secondaryRouteAnimation |
| : CurvedAnimation( |
| parent: secondaryRouteAnimation, |
| curve: Curves.linearToEaseOut, |
| reverseCurve: Curves.easeInToLinear, |
| ) |
| ).drive(_kMiddleLeftTween), |
| super(key: key); |
| |
| final Animation<Offset> _positionAnimation; |
| // When this page is becoming covered by another page. |
| final Animation<Offset> _secondaryPositionAnimation; |
| |
| /// The widget below this widget in the tree. |
| final Widget child; |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasDirectionality(context)); |
| final TextDirection textDirection = Directionality.of(context); |
| return SlideTransition( |
| position: _secondaryPositionAnimation, |
| textDirection: textDirection, |
| transformHitTests: false, |
| child: SlideTransition( |
| position: _positionAnimation, |
| child: child, |
| ), |
| ); |
| } |
| } |
| |
| /// 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. |
| /// |
| /// The gesture data is converted from absolute coordinates to logical |
| /// coordinates by this widget. |
| /// |
| /// The type `T` specifies the return type of the route with which this gesture |
| /// detector is associated. |
| class _CupertinoBackGestureDetector<T> 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<T>> onStartPopGesture; |
| |
| @override |
| _CupertinoBackGestureDetectorState<T> createState() => _CupertinoBackGestureDetectorState<T>(); |
| } |
| |
| class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureDetector<T>> { |
| _CupertinoBackGestureController<T> _backGestureController; |
| |
| HorizontalDragGestureRecognizer _recognizer; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _recognizer = 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); |
| _backGestureController.dragUpdate(_convertToLogical(details.primaryDelta / context.size.width)); |
| } |
| |
| void _handleDragEnd(DragEndDetails details) { |
| assert(mounted); |
| assert(_backGestureController != null); |
| _backGestureController.dragEnd(_convertToLogical(details.velocity.pixelsPerSecond.dx / context.size.width)); |
| _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); |
| } |
| |
| double _convertToLogical(double value) { |
| switch (Directionality.of(context)) { |
| case TextDirection.rtl: |
| return -value; |
| case TextDirection.ltr: |
| return value; |
| } |
| return null; |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasDirectionality(context)); |
| // 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 ? |
| MediaQuery.of(context).padding.left : |
| MediaQuery.of(context).padding.right; |
| dragAreaWidth = max(dragAreaWidth, _kBackGestureWidth); |
| return Stack( |
| fit: StackFit.passthrough, |
| children: <Widget>[ |
| widget.child, |
| PositionedDirectional( |
| start: 0.0, |
| width: dragAreaWidth, |
| top: 0.0, |
| bottom: 0.0, |
| child: Listener( |
| onPointerDown: _handlePointerDown, |
| behavior: HitTestBehavior.translucent, |
| ), |
| ), |
| ], |
| ); |
| } |
| } |
| |
| /// A controller for an iOS-style back gesture. |
| /// |
| /// 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. |
| /// |
| /// This class works entirely in logical coordinates (0.0 is new page dismissed, |
| /// 1.0 is new page on top). |
| /// |
| /// The type `T` specifies the return type of the route with which this gesture |
| /// detector controller is associated. |
| class _CupertinoBackGestureController<T> { |
| /// Creates a controller for an iOS-style back gesture. |
| /// |
| /// The [navigator] and [controller] arguments must not be null. |
| _CupertinoBackGestureController({ |
| @required this.navigator, |
| @required this.controller, |
| }) : assert(navigator != null), |
| assert(controller != null) { |
| navigator.didStartUserGesture(); |
| } |
| |
| final AnimationController controller; |
| final NavigatorState navigator; |
| |
| /// The drag gesture has changed by [fractionalDelta]. The total range of the |
| /// drag should be 0.0 to 1.0. |
| void dragUpdate(double delta) { |
| controller.value -= delta; |
| } |
| |
| /// 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. |
| // |
| // This curve has been determined through rigorously eyeballing native iOS |
| // animations. |
| const Curve animationCurve = Curves.fastLinearToSlowEaseIn; |
| bool animateForward; |
| |
| // 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) |
| animateForward = velocity <= 0; |
| else |
| animateForward = controller.value > 0.5; |
| |
| 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. |
| final int droppedPageForwardAnimationTime = min( |
| lerpDouble(_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value).floor(), |
| _kMaxPageBackAnimationTime, |
| ); |
| controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve); |
| } else { |
| // 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. |
| final int droppedPageBackAnimationTime = lerpDouble(0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value).floor(); |
| controller.animateBack(0.0, duration: Duration(milliseconds: droppedPageBackAnimationTime), curve: animationCurve); |
| } |
| } |
| |
| if (controller.isAnimating) { |
| // Keep the userGestureInProgress in true state so we don't change the |
| // curve of the page transition mid-flight since CupertinoPageTransition |
| // depends on userGestureInProgress. |
| AnimationStatusListener animationStatusCallback; |
| animationStatusCallback = (AnimationStatus status) { |
| navigator.didStopUserGesture(); |
| controller.removeStatusListener(animationStatusCallback); |
| }; |
| controller.addStatusListener(animationStatusCallback); |
| } else { |
| navigator.didStopUserGesture(); |
| } |
| } |
| } |
| |
| // 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). |
| class _CupertinoEdgeShadowDecoration extends Decoration { |
| const _CupertinoEdgeShadowDecoration({ this.edgeGradient }); |
| |
| // An edge shadow decoration where the shadow is null. This is used |
| // for interpolating from no shadow. |
| static const _CupertinoEdgeShadowDecoration none = |
| _CupertinoEdgeShadowDecoration(); |
| |
| // 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. |
| final LinearGradient edgeGradient; |
| |
| // 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]. |
| static _CupertinoEdgeShadowDecoration lerp( |
| _CupertinoEdgeShadowDecoration a, |
| _CupertinoEdgeShadowDecoration b, |
| double t, |
| ) { |
| assert(t != null); |
| if (a == null && b == null) |
| return null; |
| return _CupertinoEdgeShadowDecoration( |
| edgeGradient: LinearGradient.lerp(a?.edgeGradient, b?.edgeGradient, t), |
| ); |
| } |
| |
| @override |
| _CupertinoEdgeShadowDecoration lerpFrom(Decoration a, double t) { |
| if (a is _CupertinoEdgeShadowDecoration) |
| return _CupertinoEdgeShadowDecoration.lerp(a, this, t); |
| return _CupertinoEdgeShadowDecoration.lerp(null, this, t); |
| } |
| |
| @override |
| _CupertinoEdgeShadowDecoration lerpTo(Decoration b, double t) { |
| if (b is _CupertinoEdgeShadowDecoration) |
| return _CupertinoEdgeShadowDecoration.lerp(this, b, t); |
| return _CupertinoEdgeShadowDecoration.lerp(this, null, t); |
| } |
| |
| @override |
| _CupertinoEdgeShadowPainter createBoxPainter([ VoidCallback onChanged ]) { |
| return _CupertinoEdgeShadowPainter(this, onChanged); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (other.runtimeType != runtimeType) |
| return false; |
| return other is _CupertinoEdgeShadowDecoration |
| && other.edgeGradient == edgeGradient; |
| } |
| |
| @override |
| int get hashCode => edgeGradient.hashCode; |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<LinearGradient>('edgeGradient', edgeGradient)); |
| } |
| } |
| |
| /// A [BoxPainter] used to draw the page transition shadow using gradients. |
| class _CupertinoEdgeShadowPainter extends BoxPainter { |
| _CupertinoEdgeShadowPainter( |
| this._decoration, |
| VoidCallback onChange, |
| ) : 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 |
| // 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); |
| final Paint paint = Paint() |
| ..shader = gradient.createShader(rect, textDirection: textDirection); |
| |
| canvas.drawRect(rect, paint); |
| } |
| } |
| |
| class _CupertinoModalPopupRoute<T> extends PopupRoute<T> { |
| _CupertinoModalPopupRoute({ |
| this.barrierColor, |
| this.barrierLabel, |
| this.builder, |
| bool semanticsDismissible, |
| ImageFilter filter, |
| RouteSettings settings, |
| }) : super( |
| filter: filter, |
| settings: settings, |
| ) { |
| _semanticsDismissible = semanticsDismissible; |
| } |
| |
| final WidgetBuilder builder; |
| bool _semanticsDismissible; |
| |
| @override |
| final String barrierLabel; |
| |
| @override |
| final Color barrierColor; |
| |
| @override |
| bool get barrierDismissible => true; |
| |
| @override |
| bool get semanticsDismissible => _semanticsDismissible ?? false; |
| |
| @override |
| Duration get transitionDuration => _kModalPopupTransitionDuration; |
| |
| Animation<double> _animation; |
| |
| Tween<Offset> _offsetTween; |
| |
| @override |
| Animation<double> createAnimation() { |
| assert(_animation == null); |
| _animation = CurvedAnimation( |
| parent: super.createAnimation(), |
| |
| // 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, |
| ); |
| _offsetTween = Tween<Offset>( |
| 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 CupertinoUserInterfaceLevel( |
| data: CupertinoUserInterfaceLevelData.elevated, |
| child: Builder(builder: builder), |
| ); |
| } |
| |
| @override |
| Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { |
| return Align( |
| alignment: Alignment.bottomCenter, |
| child: FractionalTranslation( |
| 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 `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. |
| /// |
| /// The `semanticsDismissible` argument is used to determine whether the |
| /// semantics of the modal barrier are included in the semantics tree. |
| /// |
| /// 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: |
| /// |
| /// * [CupertinoActionSheet], 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, |
| ImageFilter filter, |
| bool useRootNavigator = true, |
| bool semanticsDismissible, |
| }) { |
| assert(useRootNavigator != null); |
| return Navigator.of(context, rootNavigator: useRootNavigator).push( |
| _CupertinoModalPopupRoute<T>( |
| barrierColor: CupertinoDynamicColor.resolve(_kModalBarrierColor, context), |
| barrierLabel: 'Dismiss', |
| builder: builder, |
| filter: filter, |
| semanticsDismissible: semanticsDismissible, |
| ), |
| ); |
| } |
| |
| // The curve and initial scale values were mostly eyeballed from iOS, however |
| // they reuse the same animation curve that was modeled after native page |
| // transitions. |
| final Animatable<double> _dialogScaleTween = Tween<double>(begin: 1.3, end: 1.0) |
| .chain(CurveTween(curve: Curves.linearToEaseOut)); |
| |
| Widget _buildCupertinoDialogTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { |
| final CurvedAnimation fadeAnimation = CurvedAnimation( |
| parent: animation, |
| curve: Curves.easeInOut, |
| ); |
| if (animation.status == AnimationStatus.reverse) { |
| return FadeTransition( |
| opacity: fadeAnimation, |
| child: child, |
| ); |
| } |
| return FadeTransition( |
| opacity: fadeAnimation, |
| child: ScaleTransition( |
| child: child, |
| scale: animation.drive(_dialogScaleTween), |
| ), |
| ); |
| } |
| |
| /// 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. |
| /// |
| /// 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. |
| /// |
| /// 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)`. |
| /// |
| /// Returns a [Future] that resolves to the value (if any) that was passed to |
| /// [Navigator.pop] when the dialog was closed. |
| /// |
| /// 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, |
| bool useRootNavigator = true, |
| RouteSettings routeSettings, |
| }) { |
| assert(builder != null); |
| assert(useRootNavigator != null); |
| return showGeneralDialog( |
| context: context, |
| barrierDismissible: false, |
| barrierColor: CupertinoDynamicColor.resolve(_kModalBarrierColor, context), |
| // This transition duration was eyeballed comparing with iOS |
| transitionDuration: const Duration(milliseconds: 250), |
| pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| return builder(context); |
| }, |
| transitionBuilder: _buildCupertinoDialogTransitions, |
| useRootNavigator: useRootNavigator, |
| routeSettings: routeSettings, |
| ); |
| } |