| // 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:math'; |
| import 'dart:ui' show ImageFilter, lerpDouble; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'colors.dart'; |
| import 'interface_level.dart'; |
| import 'localizations.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 used for a barrier visible during transitions for Cupertino |
| /// page routes. |
| /// |
| /// This barrier color is only used for full-screen page routes with |
| /// `fullscreenDialog: false`. |
| /// |
| /// By default, `fullscreenDialog` Cupertino route transitions have no |
| /// `barrierColor`, and [CupertinoDialogRoute]s and [CupertinoModalPopupRoute]s |
| /// have a `barrierColor` defined by [kCupertinoModalBarrierColor]. |
| /// |
| /// A relatively rigorous eyeball estimation. |
| const Color _kCupertinoPageTransitionBarrierColor = Color(0x18000000); |
| |
| /// Barrier color for a Cupertino modal barrier. |
| /// |
| /// Extracted from https://developer.apple.com/design/resources/. |
| const Color kCupertinoModalBarrierColor = 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, |
| ); |
| |
| /// A mixin that replaces the entire screen with an iOS transition for a |
| /// [PageRoute]. |
| /// |
| /// {@template flutter.cupertino.cupertinoRouteTransitionMixin} |
| /// 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. |
| /// {@endtemplate} |
| /// |
| /// See also: |
| /// |
| /// * [MaterialRouteTransitionMixin], which is a mixin that provides |
| /// platform-appropriate transitions for a [PageRoute]. |
| /// * [CupertinoPageRoute], which is a [PageRoute] that leverages this mixin. |
| mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> { |
| /// Builds the primary contents of the route. |
| @protected |
| Widget buildContent(BuildContext context); |
| |
| /// {@template flutter.cupertino.CupertinoRouteTransitionMixin.title} |
| /// A title string for this route. |
| /// |
| /// Used to auto-populate [CupertinoNavigationBar] and |
| /// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when |
| /// one is not manually supplied. |
| /// {@endtemplate} |
| String? get 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 CupertinoRouteTransitionMixin |
| ? previousRoute.title |
| : null; |
| if (_previousTitle == null) { |
| _previousTitle = ValueNotifier<String?>(previousTitleString); |
| } else { |
| _previousTitle!.value = previousTitleString; |
| } |
| super.didChangePrevious(previousRoute); |
| } |
| |
| @override |
| // A relatively rigorous eyeball estimation. |
| Duration get transitionDuration => const Duration(milliseconds: 500); |
| |
| @override |
| Color? get barrierColor => fullscreenDialog ? null : _kCupertinoPageTransitionBarrierColor; |
| |
| @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 CupertinoRouteTransitionMixin && !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 = buildContent(context); |
| return Semantics( |
| scopesRoute: true, |
| explicitChildNodes: true, |
| child: child, |
| ); |
| } |
| |
| // 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, |
| linearTransition: linearTransition, |
| child: child, |
| ); |
| } 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); |
| } |
| } |
| |
| /// A modal route that replaces the entire screen with an iOS transition. |
| /// |
| /// {@macro flutter.cupertino.cupertinoRouteTransitionMixin} |
| /// |
| /// By default, when a modal route is replaced by another, the previous route |
| /// remains in memory. To free all the resources when this is not necessary, set |
| /// [maintainState] to false. |
| /// |
| /// The type `T` specifies the return type of the route which can be supplied as |
| /// the route is popped from the stack via [Navigator.pop] when an optional |
| /// `result` can be provided. |
| /// |
| /// See also: |
| /// |
| /// * [CupertinoRouteTransitionMixin], for a mixin that provides iOS transition |
| /// for this modal route. |
| /// * [MaterialPageRoute], for an adaptive [PageRoute] that uses a |
| /// platform-appropriate transition. |
| /// * [CupertinoPageScaffold], for applications that have one page with a fixed |
| /// navigation bar on top. |
| /// * [CupertinoTabScaffold], for applications that have a tab bar at the |
| /// bottom with multiple pages. |
| /// * [CupertinoPage], for a [Page] version of this class. |
| class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> { |
| /// Creates a page route for use in an iOS designed app. |
| /// |
| /// The [builder], [maintainState], and [fullscreenDialog] arguments must not |
| /// be null. |
| CupertinoPageRoute({ |
| required this.builder, |
| this.title, |
| super.settings, |
| this.maintainState = true, |
| super.fullscreenDialog, |
| super.allowSnapshotting = true, |
| }) { |
| assert(opaque); |
| } |
| |
| /// Builds the primary contents of the route. |
| final WidgetBuilder builder; |
| |
| @override |
| Widget buildContent(BuildContext context) => builder(context); |
| |
| @override |
| final String? title; |
| |
| @override |
| final bool maintainState; |
| |
| @override |
| String get debugLabel => '${super.debugLabel}(${settings.name})'; |
| } |
| |
| // A page-based version of CupertinoPageRoute. |
| // |
| // This route uses the builder from the page to build its content. This ensures |
| // the content is up to date after page updates. |
| class _PageBasedCupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> { |
| _PageBasedCupertinoPageRoute({ |
| required CupertinoPage<T> page, |
| super.allowSnapshotting = true, |
| }) : super(settings: page) { |
| assert(opaque); |
| } |
| |
| CupertinoPage<T> get _page => settings as CupertinoPage<T>; |
| |
| @override |
| Widget buildContent(BuildContext context) => _page.child; |
| |
| @override |
| String? get title => _page.title; |
| |
| @override |
| bool get maintainState => _page.maintainState; |
| |
| @override |
| bool get fullscreenDialog => _page.fullscreenDialog; |
| |
| @override |
| String get debugLabel => '${super.debugLabel}(${_page.name})'; |
| } |
| |
| /// A page that creates a cupertino style [PageRoute]. |
| /// |
| /// {@macro flutter.cupertino.cupertinoRouteTransitionMixin} |
| /// |
| /// By default, when a created modal route is replaced by another, the previous |
| /// route remains in memory. To free all the resources when this is not |
| /// necessary, set [maintainState] to false. |
| /// |
| /// The type `T` specifies the return type of the route which can be supplied as |
| /// the route is popped from the stack via [Navigator.transitionDelegate] by |
| /// providing the optional `result` argument to the |
| /// [RouteTransitionRecord.markForPop] in the [TransitionDelegate.resolve]. |
| /// |
| /// See also: |
| /// |
| /// * [CupertinoPageRoute], for a [PageRoute] version of this class. |
| class CupertinoPage<T> extends Page<T> { |
| /// Creates a cupertino page. |
| const CupertinoPage({ |
| required this.child, |
| this.maintainState = true, |
| this.title, |
| this.fullscreenDialog = false, |
| this.allowSnapshotting = true, |
| super.key, |
| super.name, |
| super.arguments, |
| super.restorationId, |
| }); |
| |
| /// The content to be shown in the [Route] created by this page. |
| final Widget child; |
| |
| /// {@macro flutter.cupertino.CupertinoRouteTransitionMixin.title} |
| final String? title; |
| |
| /// {@macro flutter.widgets.ModalRoute.maintainState} |
| final bool maintainState; |
| |
| /// {@macro flutter.widgets.PageRoute.fullscreenDialog} |
| final bool fullscreenDialog; |
| |
| /// {@macro flutter.widgets.TransitionRoute.allowSnapshotting} |
| final bool allowSnapshotting; |
| |
| @override |
| Route<T> createRoute(BuildContext context) { |
| return _PageBasedCupertinoPageRoute<T>(page: this, allowSnapshotting: allowSnapshotting); |
| } |
| } |
| |
| /// 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({ |
| super.key, |
| required Animation<double> primaryRouteAnimation, |
| required Animation<double> secondaryRouteAnimation, |
| required this.child, |
| required bool linearTransition, |
| }) : _primaryPositionAnimation = |
| (linearTransition |
| ? primaryRouteAnimation |
| : CurvedAnimation( |
| parent: primaryRouteAnimation, |
| curve: Curves.fastEaseInToSlowEaseOut, |
| reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, |
| ) |
| ).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(_CupertinoEdgeShadowDecoration.kTween); |
| |
| // 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({ |
| super.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); |
| |
| 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({ |
| super.key, |
| required this.enabledCallback, |
| required this.onStartPopGesture, |
| required this.child, |
| }); |
| |
| 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; |
| |
| late 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; |
| } |
| } |
| |
| @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.paddingOf(context).left : |
| MediaQuery.paddingOf(context).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, |
| }) { |
| 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. |
| // |
| // This curve has been determined through rigorously eyeballing native iOS |
| // animations. |
| const Curve animationCurve = Curves.fastLinearToSlowEaseIn; |
| final 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. |
| late 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. |
| class _CupertinoEdgeShadowDecoration extends Decoration { |
| const _CupertinoEdgeShadowDecoration._([this._colors]); |
| |
| static DecorationTween kTween = DecorationTween( |
| begin: const _CupertinoEdgeShadowDecoration._(), // No decoration initially. |
| end: const _CupertinoEdgeShadowDecoration._( |
| // Eyeballed gradient used to mimic a drop shadow on the start side only. |
| <Color>[ |
| Color(0x04000000), |
| Color(0x00000000), |
| ], |
| ), |
| ); |
| |
| // Colors used to paint a gradient at the start edge of the box it is |
| // decorating. |
| // |
| // The first color in the list is used at the start of the gradient, which |
| // is located at the start edge of the decorated box. |
| // |
| // If this is null, no shadow is drawn. |
| // |
| // The list must have at least two colors in it (otherwise it would not be a |
| // gradient). |
| final List<Color>? _colors; |
| |
| // 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, |
| ) { |
| if (identical(a, b)) { |
| return a; |
| } |
| if (a == null) { |
| return b!._colors == null ? b : _CupertinoEdgeShadowDecoration._(b._colors!.map<Color>((Color color) => Color.lerp(null, color, t)!).toList()); |
| } |
| if (b == null) { |
| return a._colors == null ? a : _CupertinoEdgeShadowDecoration._(a._colors!.map<Color>((Color color) => Color.lerp(null, color, 1.0 - t)!).toList()); |
| } |
| assert(b._colors != null || a._colors != null); |
| // If it ever becomes necessary, we could allow decorations with different |
| // length' here, similarly to how it is handled in [LinearGradient.lerp]. |
| assert(b._colors == null || a._colors == null || a._colors!.length == b._colors!.length); |
| return _CupertinoEdgeShadowDecoration._( |
| <Color>[ |
| for (int i = 0; i < b._colors!.length; i += 1) |
| Color.lerp(a._colors?[i], b._colors?[i], t)!, |
| ], |
| ); |
| } |
| |
| @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._colors == _colors; |
| } |
| |
| @override |
| int get hashCode => _colors.hashCode; |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(IterableProperty<Color>('colors', _colors)); |
| } |
| } |
| |
| /// A [BoxPainter] used to draw the page transition shadow using gradients. |
| class _CupertinoEdgeShadowPainter extends BoxPainter { |
| _CupertinoEdgeShadowPainter( |
| this._decoration, |
| super.onChanged, |
| ) : assert(_decoration._colors == null || _decoration._colors!.length > 1); |
| |
| final _CupertinoEdgeShadowDecoration _decoration; |
| |
| @override |
| void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { |
| final List<Color>? colors = _decoration._colors; |
| if (colors == null) { |
| return; |
| } |
| |
| // The following code simulates drawing a [LinearGradient] configured as |
| // follows: |
| // |
| // LinearGradient( |
| // begin: AlignmentDirectional(0.90, 0.0), // Spans 5% of the page. |
| // colors: _decoration._colors, |
| // ) |
| // |
| // A performance evaluation on Feb 8, 2021 showed, that drawing the gradient |
| // manually as implemented below is more performant than relying on |
| // [LinearGradient.createShader] because compiling that shader takes a long |
| // time. On an iPhone XR, the implementation below reduced the worst frame |
| // time for a cupertino page transition of a newly installed app from ~95ms |
| // down to ~30ms, mainly because there's no longer a need to compile a |
| // shader for the LinearGradient. |
| // |
| // The implementation below divides the width of the shadow into multiple |
| // bands of equal width, one for each color interval defined by |
| // `_decoration._colors`. Band x is filled with a gradient going from |
| // `_decoration._colors[x]` to `_decoration._colors[x + 1]` by drawing a |
| // bunch of 1px wide rects. The rects change their color by lerping between |
| // the two colors that define the interval of the band. |
| |
| // Shadow spans 5% of the page. |
| final double shadowWidth = 0.05 * configuration.size!.width; |
| final double shadowHeight = configuration.size!.height; |
| final double bandWidth = shadowWidth / (colors.length - 1); |
| |
| final TextDirection? textDirection = configuration.textDirection; |
| assert(textDirection != null); |
| final double start; |
| final double shadowDirection; // -1 for ltr, 1 for rtl. |
| switch (textDirection!) { |
| case TextDirection.rtl: |
| start = offset.dx + configuration.size!.width; |
| shadowDirection = 1; |
| case TextDirection.ltr: |
| start = offset.dx; |
| shadowDirection = -1; |
| } |
| |
| int bandColorIndex = 0; |
| for (int dx = 0; dx < shadowWidth; dx += 1) { |
| if (dx ~/ bandWidth != bandColorIndex) { |
| bandColorIndex += 1; |
| } |
| final Paint paint = Paint() |
| ..color = Color.lerp(colors[bandColorIndex], colors[bandColorIndex + 1], (dx % bandWidth) / bandWidth)!; |
| final double x = start + shadowDirection * dx; |
| canvas.drawRect(Rect.fromLTWH(x - 1.0, offset.dy, 1.0, shadowHeight), paint); |
| } |
| } |
| } |
| |
| /// A route that shows a modal iOS-style popup that slides up from the |
| /// bottom of the screen. |
| /// |
| /// Such a popup is an alternative to a menu or a dialog and prevents the user |
| /// from interacting with the rest of the app. |
| /// |
| /// It is used internally by [showCupertinoModalPopup] or can be directly pushed |
| /// onto the [Navigator] stack to enable state restoration. See |
| /// [showCupertinoModalPopup] for a state restoration app example. |
| /// |
| /// The `barrierColor` argument determines the [Color] of the barrier underneath |
| /// the popup. When unspecified, the barrier color defaults to a light opacity |
| /// black scrim based on iOS's dialog screens. To correctly have iOS resolve |
| /// to the appropriate modal colors, pass in |
| /// `CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context)`. |
| /// |
| /// The `barrierDismissible` argument determines whether clicking outside the |
| /// popup results in dismissal. It is `true` by default. |
| /// |
| /// The `semanticsDismissible` argument is used to determine whether the |
| /// semantics of the modal barrier are included in the semantics tree. |
| /// |
| /// The `routeSettings` argument is used to provide [RouteSettings] to the |
| /// created Route. |
| /// |
| /// {@macro flutter.widgets.RawDialogRoute} |
| /// |
| /// See also: |
| /// |
| /// * [DisplayFeatureSubScreen], which documents the specifics of how |
| /// [DisplayFeature]s can split the screen into sub-screens. |
| /// * [CupertinoActionSheet], which is the widget usually returned by the |
| /// `builder` argument. |
| /// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/> |
| class CupertinoModalPopupRoute<T> extends PopupRoute<T> { |
| /// A route that shows a modal iOS-style popup that slides up from the |
| /// bottom of the screen. |
| CupertinoModalPopupRoute({ |
| required this.builder, |
| this.barrierLabel = 'Dismiss', |
| this.barrierColor = kCupertinoModalBarrierColor, |
| bool barrierDismissible = true, |
| bool semanticsDismissible = false, |
| super.filter, |
| super.settings, |
| this.anchorPoint, |
| }) : _barrierDismissible = barrierDismissible, |
| _semanticsDismissible = semanticsDismissible; |
| |
| /// A builder that builds the widget tree for the [CupertinoModalPopupRoute]. |
| /// |
| /// The [builder] argument typically builds a [CupertinoActionSheet] widget. |
| /// |
| /// Content below the widget is dimmed with a [ModalBarrier]. The widget built |
| /// by the [builder] does not share a context with the route it was originally |
| /// built from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the |
| /// widget needs to update dynamically. |
| final WidgetBuilder builder; |
| |
| final bool _barrierDismissible; |
| |
| final bool _semanticsDismissible; |
| |
| @override |
| final String barrierLabel; |
| |
| @override |
| final Color? barrierColor; |
| |
| @override |
| bool get barrierDismissible => _barrierDismissible; |
| |
| @override |
| bool get semanticsDismissible => _semanticsDismissible; |
| |
| @override |
| Duration get transitionDuration => _kModalPopupTransitionDuration; |
| |
| Animation<double>? _animation; |
| |
| late Tween<Offset> _offsetTween; |
| |
| /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint} |
| final Offset? anchorPoint; |
| |
| @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: Offset.zero, |
| ); |
| return _animation!; |
| } |
| |
| @override |
| Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| return CupertinoUserInterfaceLevel( |
| data: CupertinoUserInterfaceLevelData.elevated, |
| child: DisplayFeatureSubScreen( |
| anchorPoint: anchorPoint, |
| 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 `barrierColor` argument determines the [Color] of the barrier underneath |
| /// the popup. When unspecified, the barrier color defaults to a light opacity |
| /// black scrim based on iOS's dialog screens. |
| /// |
| /// The `barrierDismissible` argument determines whether clicking outside the |
| /// popup results in dismissal. It is `true` by default. |
| /// |
| /// 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 `true` by default. |
| /// |
| /// The `semanticsDismissible` argument is used to determine whether the |
| /// semantics of the modal barrier are included in the semantics tree. |
| /// |
| /// The `routeSettings` argument is used to provide [RouteSettings] to the |
| /// created Route. |
| /// |
| /// 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. |
| /// |
| /// {@macro flutter.widgets.RawDialogRoute} |
| /// |
| /// Returns a `Future` that resolves to the value that was passed to |
| /// [Navigator.pop] when the popup was closed. |
| /// |
| /// ### State Restoration in Modals |
| /// |
| /// Using this method will not enable state restoration for the modal. In order |
| /// to enable state restoration for a modal, use [Navigator.restorablePush] |
| /// or [Navigator.restorablePushNamed] with [CupertinoModalPopupRoute]. |
| /// |
| /// For more information about state restoration, see [RestorationManager]. |
| /// |
| /// {@tool dartpad} |
| /// This sample demonstrates how to create a restorable Cupertino modal route. |
| /// This is accomplished by enabling state restoration by specifying |
| /// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to |
| /// push [CupertinoModalPopupRoute] when the [CupertinoButton] is tapped. |
| /// |
| /// {@macro flutter.widgets.RestorationManager} |
| /// |
| /// ** See code in examples/api/lib/cupertino/route/show_cupertino_modal_popup.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [DisplayFeatureSubScreen], which documents the specifics of how |
| /// [DisplayFeature]s can split the screen into sub-screens. |
| /// * [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, |
| Color barrierColor = kCupertinoModalBarrierColor, |
| bool barrierDismissible = true, |
| bool useRootNavigator = true, |
| bool semanticsDismissible = false, |
| RouteSettings? routeSettings, |
| Offset? anchorPoint, |
| }) { |
| return Navigator.of(context, rootNavigator: useRootNavigator).push( |
| CupertinoModalPopupRoute<T>( |
| builder: builder, |
| filter: filter, |
| barrierColor: CupertinoDynamicColor.resolve(barrierColor, context), |
| barrierDismissible: barrierDismissible, |
| semanticsDismissible: semanticsDismissible, |
| settings: routeSettings, |
| anchorPoint: anchorPoint, |
| ), |
| ); |
| } |
| |
| // 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( |
| scale: animation.drive(_dialogScaleTween), |
| child: child, |
| ), |
| ); |
| } |
| |
| /// 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 (by default, the dialog is not dismissible with a tap on |
| /// the barrier). |
| /// |
| /// This function takes a `builder` which typically builds a [CupertinoAlertDialog] |
| /// widget. Content below the dialog is dimmed with a [ModalBarrier]. The widget |
| /// returned by the `builder` does not share a context with the location that |
| /// [showCupertinoDialog] is originally called from. Use a [StatefulBuilder] or |
| /// a custom [StatefulWidget] if the dialog needs to update dynamically. |
| /// |
| /// 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. |
| /// |
| /// {@macro flutter.widgets.RawDialogRoute} |
| /// |
| /// 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. |
| /// |
| /// ### State Restoration in Dialogs |
| /// |
| /// Using this method will not enable state restoration for the dialog. In order |
| /// to enable state restoration for a dialog, use [Navigator.restorablePush] |
| /// or [Navigator.restorablePushNamed] with [CupertinoDialogRoute]. |
| /// |
| /// For more information about state restoration, see [RestorationManager]. |
| /// |
| /// {@tool dartpad} |
| /// This sample demonstrates how to create a restorable Cupertino dialog. This is |
| /// accomplished by enabling state restoration by specifying |
| /// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to |
| /// push [CupertinoDialogRoute] when the [CupertinoButton] is tapped. |
| /// |
| /// {@macro flutter.widgets.RestorationManager} |
| /// |
| /// ** See code in examples/api/lib/cupertino/route/show_cupertino_dialog.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [CupertinoAlertDialog], an iOS-style alert dialog. |
| /// * [showDialog], which displays a Material-style dialog. |
| /// * [showGeneralDialog], which allows for customization of the dialog popup. |
| /// * [DisplayFeatureSubScreen], which documents the specifics of how |
| /// [DisplayFeature]s can split the screen into sub-screens. |
| /// * <https://developer.apple.com/ios/human-interface-guidelines/views/alerts/> |
| Future<T?> showCupertinoDialog<T>({ |
| required BuildContext context, |
| required WidgetBuilder builder, |
| String? barrierLabel, |
| bool useRootNavigator = true, |
| bool barrierDismissible = false, |
| RouteSettings? routeSettings, |
| Offset? anchorPoint, |
| }) { |
| |
| return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(CupertinoDialogRoute<T>( |
| builder: builder, |
| context: context, |
| barrierDismissible: barrierDismissible, |
| barrierLabel: barrierLabel, |
| barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context), |
| settings: routeSettings, |
| anchorPoint: anchorPoint, |
| )); |
| } |
| |
| /// A dialog route that shows an iOS-style dialog. |
| /// |
| /// It is used internally by [showCupertinoDialog] or can be directly pushed |
| /// onto the [Navigator] stack to enable state restoration. See |
| /// [showCupertinoDialog] for a state restoration app example. |
| /// |
| /// This function takes a `builder` which typically builds a [Dialog] widget. |
| /// Content below the dialog is dimmed with a [ModalBarrier]. The widget |
| /// returned by the `builder` does not share a context with the location that |
| /// `showDialog` is originally called from. Use a [StatefulBuilder] or a |
| /// custom [StatefulWidget] if the dialog needs to update dynamically. |
| /// |
| /// The `context` argument is used to look up |
| /// [CupertinoLocalizations.modalBarrierDismissLabel], which provides the |
| /// modal with a localized accessibility label that will be used for the |
| /// modal's barrier. However, a custom `barrierLabel` can be passed in as well. |
| /// |
| /// The `barrierDismissible` argument is used to indicate whether tapping on the |
| /// barrier will dismiss the dialog. It is `true` by default and cannot be `null`. |
| /// |
| /// The `barrierColor` argument is used to specify the color of the modal |
| /// barrier that darkens everything below the dialog. If `null`, then |
| /// [CupertinoDynamicColor.resolve] is used to compute the modal color. |
| /// |
| /// The `settings` argument define the settings for this route. See |
| /// [RouteSettings] for details. |
| /// |
| /// {@macro flutter.widgets.RawDialogRoute} |
| /// |
| /// See also: |
| /// |
| /// * [showCupertinoDialog], which is a way to display |
| /// an iOS-style dialog. |
| /// * [showGeneralDialog], which allows for customization of the dialog popup. |
| /// * [showDialog], which displays a Material dialog. |
| /// * [DisplayFeatureSubScreen], which documents the specifics of how |
| /// [DisplayFeature]s can split the screen into sub-screens. |
| class CupertinoDialogRoute<T> extends RawDialogRoute<T> { |
| /// A dialog route that shows an iOS-style dialog. |
| CupertinoDialogRoute({ |
| required WidgetBuilder builder, |
| required BuildContext context, |
| super.barrierDismissible, |
| Color? barrierColor, |
| String? barrierLabel, |
| // This transition duration was eyeballed comparing with iOS |
| super.transitionDuration = const Duration(milliseconds: 250), |
| super.transitionBuilder = _buildCupertinoDialogTransitions, |
| super.settings, |
| super.anchorPoint, |
| }) : super( |
| pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| return builder(context); |
| }, |
| barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel, |
| barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context), |
| ); |
| } |