1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'dart:ui'; |
6 | /// |
7 | /// @docImport 'package:flutter/material.dart'; |
8 | /// @docImport 'package:flutter/services.dart'; |
9 | /// |
10 | /// @docImport 'app.dart'; |
11 | /// @docImport 'button.dart'; |
12 | /// @docImport 'dialog.dart'; |
13 | /// @docImport 'nav_bar.dart'; |
14 | /// @docImport 'page_scaffold.dart'; |
15 | /// @docImport 'tab_scaffold.dart'; |
16 | library; |
17 | |
18 | import 'dart:math'; |
19 | import 'dart:ui' show ImageFilter; |
20 | |
21 | import 'package:flutter/foundation.dart'; |
22 | import 'package:flutter/gestures.dart'; |
23 | import 'package:flutter/rendering.dart'; |
24 | import 'package:flutter/widgets.dart'; |
25 | |
26 | import 'colors.dart'; |
27 | import 'interface_level.dart'; |
28 | import 'localizations.dart'; |
29 | |
30 | const double _kBackGestureWidth = 20.0; |
31 | const double _kMinFlingVelocity = 1.0; // Screen widths per second. |
32 | |
33 | // The duration for a page to animate when the user releases it mid-swipe. |
34 | const Duration _kDroppedSwipePageAnimationDuration = Duration(milliseconds: 350); |
35 | |
36 | /// Barrier color used for a barrier visible during transitions for Cupertino |
37 | /// page routes. |
38 | /// |
39 | /// This barrier color is only used for full-screen page routes with |
40 | /// `fullscreenDialog: false`. |
41 | /// |
42 | /// By default, `fullscreenDialog` Cupertino route transitions have no |
43 | /// `barrierColor`, and [CupertinoDialogRoute]s and [CupertinoModalPopupRoute]s |
44 | /// have a `barrierColor` defined by [kCupertinoModalBarrierColor]. |
45 | /// |
46 | /// A relatively rigorous eyeball estimation. |
47 | const Color _kCupertinoPageTransitionBarrierColor = Color(0x18000000); |
48 | |
49 | /// Barrier color for a Cupertino modal barrier. |
50 | /// |
51 | /// Extracted from https://developer.apple.com/design/resources/. |
52 | const Color kCupertinoModalBarrierColor = CupertinoDynamicColor.withBrightness( |
53 | color: Color(0x33000000), |
54 | darkColor: Color(0x7A000000), |
55 | ); |
56 | |
57 | // The duration of the transition used when a modal popup is shown. |
58 | const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335); |
59 | |
60 | // Offset from offscreen to the right to fully on screen. |
61 | final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( |
62 | begin: const Offset(1.0, 0.0), |
63 | end: Offset.zero, |
64 | ); |
65 | |
66 | // Offset from fully on screen to 1/3 offscreen to the left. |
67 | final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>( |
68 | begin: Offset.zero, |
69 | end: const Offset(-1.0/3.0, 0.0), |
70 | ); |
71 | |
72 | // Offset from offscreen below to fully on screen. |
73 | final Animatable<Offset> _kBottomUpTween = Tween<Offset>( |
74 | begin: const Offset(0.0, 1.0), |
75 | end: Offset.zero, |
76 | ); |
77 | |
78 | /// A mixin that replaces the entire screen with an iOS transition for a |
79 | /// [PageRoute]. |
80 | /// |
81 | /// {@template flutter.cupertino.cupertinoRouteTransitionMixin} |
82 | /// The page slides in from the right and exits in reverse. The page also shifts |
83 | /// to the left in parallax when another page enters to cover it. |
84 | /// |
85 | /// The page slides in from the bottom and exits in reverse with no parallax |
86 | /// effect for fullscreen dialogs. |
87 | /// {@endtemplate} |
88 | /// |
89 | /// See also: |
90 | /// |
91 | /// * [MaterialRouteTransitionMixin], which is a mixin that provides |
92 | /// platform-appropriate transitions for a [PageRoute]. |
93 | /// * [CupertinoPageRoute], which is a [PageRoute] that leverages this mixin. |
94 | mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> { |
95 | /// Builds the primary contents of the route. |
96 | @protected |
97 | Widget buildContent(BuildContext context); |
98 | |
99 | /// {@template flutter.cupertino.CupertinoRouteTransitionMixin.title} |
100 | /// A title string for this route. |
101 | /// |
102 | /// Used to auto-populate [CupertinoNavigationBar] and |
103 | /// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when |
104 | /// one is not manually supplied. |
105 | /// {@endtemplate} |
106 | String? get title; |
107 | |
108 | ValueNotifier<String?>? _previousTitle; |
109 | |
110 | /// The title string of the previous [CupertinoPageRoute]. |
111 | /// |
112 | /// The [ValueListenable]'s value is readable after the route is installed |
113 | /// onto a [Navigator]. The [ValueListenable] will also notify its listeners |
114 | /// if the value changes (such as by replacing the previous route). |
115 | /// |
116 | /// The [ValueListenable] itself will be null before the route is installed. |
117 | /// Its content value will be null if the previous route has no title or |
118 | /// is not a [CupertinoPageRoute]. |
119 | /// |
120 | /// See also: |
121 | /// |
122 | /// * [ValueListenableBuilder], which can be used to listen and rebuild |
123 | /// widgets based on a ValueListenable. |
124 | ValueListenable<String?> get previousTitle { |
125 | assert( |
126 | _previousTitle != null, |
127 | 'Cannot read the previousTitle for a route that has not yet been installed', |
128 | ); |
129 | return _previousTitle!; |
130 | } |
131 | |
132 | @override |
133 | void dispose() { |
134 | _previousTitle?.dispose(); |
135 | super.dispose(); |
136 | } |
137 | |
138 | @override |
139 | void didChangePrevious(Route<dynamic>? previousRoute) { |
140 | final String? previousTitleString = previousRoute is CupertinoRouteTransitionMixin |
141 | ? previousRoute.title |
142 | : null; |
143 | if (_previousTitle == null) { |
144 | _previousTitle = ValueNotifier<String?>(previousTitleString); |
145 | } else { |
146 | _previousTitle!.value = previousTitleString; |
147 | } |
148 | super.didChangePrevious(previousRoute); |
149 | } |
150 | |
151 | @override |
152 | // A relatively rigorous eyeball estimation. |
153 | Duration get transitionDuration => const Duration(milliseconds: 500); |
154 | |
155 | @override |
156 | Color? get barrierColor => fullscreenDialog ? null : _kCupertinoPageTransitionBarrierColor; |
157 | |
158 | @override |
159 | String? get barrierLabel => null; |
160 | |
161 | @override |
162 | bool canTransitionTo(TransitionRoute<dynamic> nextRoute) { |
163 | // Don't perform outgoing animation if the next route is a fullscreen dialog. |
164 | final bool nextRouteIsNotFullscreen = (nextRoute is! PageRoute<T>) || !nextRoute.fullscreenDialog; |
165 | |
166 | // If the next route has a delegated transition, then this route is able to |
167 | // use that delegated transition to smoothly sync with the next route's |
168 | // transition. |
169 | final bool nextRouteHasDelegatedTransition = nextRoute is ModalRoute<T> |
170 | && nextRoute.delegatedTransition != null; |
171 | |
172 | // Otherwise if the next route has the same route transition mixin as this |
173 | // one, then this route will already be synced with its transition. |
174 | return nextRouteIsNotFullscreen && |
175 | ((nextRoute is CupertinoRouteTransitionMixin) || nextRouteHasDelegatedTransition); |
176 | } |
177 | |
178 | @override |
179 | Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
180 | final Widget child = buildContent(context); |
181 | return Semantics( |
182 | scopesRoute: true, |
183 | explicitChildNodes: true, |
184 | child: child, |
185 | ); |
186 | } |
187 | |
188 | // Called by _CupertinoBackGestureDetector when a pop ("back") drag start |
189 | // gesture is detected. The returned controller handles all of the subsequent |
190 | // drag events. |
191 | static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) { |
192 | assert(route.popGestureEnabled); |
193 | |
194 | return _CupertinoBackGestureController<T>( |
195 | navigator: route.navigator!, |
196 | getIsCurrent: () => route.isCurrent, |
197 | getIsActive: () => route.isActive, |
198 | controller: route.controller!, // protected access |
199 | ); |
200 | } |
201 | |
202 | /// Returns a [CupertinoFullscreenDialogTransition] if [route] is a full |
203 | /// screen dialog, otherwise a [CupertinoPageTransition] is returned. |
204 | /// |
205 | /// Used by [CupertinoPageRoute.buildTransitions]. |
206 | /// |
207 | /// This method can be applied to any [PageRoute], not just |
208 | /// [CupertinoPageRoute]. It's typically used to provide a Cupertino style |
209 | /// horizontal transition for material widgets when the target platform |
210 | /// is [TargetPlatform.iOS]. |
211 | /// |
212 | /// See also: |
213 | /// |
214 | /// * [CupertinoPageTransitionsBuilder], which uses this method to define a |
215 | /// [PageTransitionsBuilder] for the [PageTransitionsTheme]. |
216 | static Widget buildPageTransitions<T>( |
217 | PageRoute<T> route, |
218 | BuildContext context, |
219 | Animation<double> animation, |
220 | Animation<double> secondaryAnimation, |
221 | Widget child, |
222 | ) { |
223 | // Check if the route has an animation that's currently participating |
224 | // in a back swipe gesture. |
225 | // |
226 | // In the middle of a back gesture drag, let the transition be linear to |
227 | // match finger motions. |
228 | final bool linearTransition = route.popGestureInProgress; |
229 | if (route.fullscreenDialog) { |
230 | return CupertinoFullscreenDialogTransition( |
231 | primaryRouteAnimation: animation, |
232 | secondaryRouteAnimation: secondaryAnimation, |
233 | linearTransition: linearTransition, |
234 | child: child, |
235 | ); |
236 | } else { |
237 | return CupertinoPageTransition( |
238 | primaryRouteAnimation: animation, |
239 | secondaryRouteAnimation: secondaryAnimation, |
240 | linearTransition: linearTransition, |
241 | child: _CupertinoBackGestureDetector<T>( |
242 | enabledCallback: () => route.popGestureEnabled, |
243 | onStartPopGesture: () => _startPopGesture<T>(route), |
244 | child: child, |
245 | ), |
246 | ); |
247 | } |
248 | } |
249 | |
250 | @override |
251 | Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { |
252 | return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child); |
253 | } |
254 | } |
255 | |
256 | /// A modal route that replaces the entire screen with an iOS transition. |
257 | /// |
258 | /// {@macro flutter.cupertino.cupertinoRouteTransitionMixin} |
259 | /// |
260 | /// By default, when a modal route is replaced by another, the previous route |
261 | /// remains in memory. To free all the resources when this is not necessary, set |
262 | /// [maintainState] to false. |
263 | /// |
264 | /// The type `T` specifies the return type of the route which can be supplied as |
265 | /// the route is popped from the stack via [Navigator.pop] when an optional |
266 | /// `result` can be provided. |
267 | /// |
268 | /// If `barrierDismissible` is true, then pressing the escape key on the keyboard |
269 | /// will cause the current route to be popped with null as the value. |
270 | /// |
271 | /// See also: |
272 | /// |
273 | /// * [CupertinoRouteTransitionMixin], for a mixin that provides iOS transition |
274 | /// for this modal route. |
275 | /// * [MaterialPageRoute], for an adaptive [PageRoute] that uses a |
276 | /// platform-appropriate transition. |
277 | /// * [CupertinoPageScaffold], for applications that have one page with a fixed |
278 | /// navigation bar on top. |
279 | /// * [CupertinoTabScaffold], for applications that have a tab bar at the |
280 | /// bottom with multiple pages. |
281 | /// * [CupertinoPage], for a [Page] version of this class. |
282 | class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> { |
283 | /// Creates a page route for use in an iOS designed app. |
284 | /// |
285 | /// The [builder], [maintainState], and [fullscreenDialog] arguments must not |
286 | /// be null. |
287 | CupertinoPageRoute({ |
288 | required this.builder, |
289 | this.title, |
290 | super.settings, |
291 | super.requestFocus, |
292 | this.maintainState = true, |
293 | super.fullscreenDialog, |
294 | super.allowSnapshotting = true, |
295 | super.barrierDismissible = false, |
296 | }) { |
297 | assert(opaque); |
298 | } |
299 | |
300 | @override |
301 | DelegatedTransitionBuilder? get delegatedTransition => CupertinoPageTransition.delegatedTransition; |
302 | |
303 | /// Builds the primary contents of the route. |
304 | final WidgetBuilder builder; |
305 | |
306 | @override |
307 | Widget buildContent(BuildContext context) => builder(context); |
308 | |
309 | @override |
310 | final String? title; |
311 | |
312 | @override |
313 | final bool maintainState; |
314 | |
315 | @override |
316 | String get debugLabel => '${super.debugLabel} (${settings.name} )'; |
317 | } |
318 | |
319 | // A page-based version of CupertinoPageRoute. |
320 | // |
321 | // This route uses the builder from the page to build its content. This ensures |
322 | // the content is up to date after page updates. |
323 | class _PageBasedCupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> { |
324 | _PageBasedCupertinoPageRoute({ |
325 | required CupertinoPage<T> page, |
326 | super.allowSnapshotting = true, |
327 | }) : super(settings: page) { |
328 | assert(opaque); |
329 | } |
330 | |
331 | @override |
332 | DelegatedTransitionBuilder? get delegatedTransition => this.fullscreenDialog ? null : CupertinoPageTransition.delegatedTransition; |
333 | |
334 | CupertinoPage<T> get _page => settings as CupertinoPage<T>; |
335 | |
336 | @override |
337 | Widget buildContent(BuildContext context) => _page.child; |
338 | |
339 | @override |
340 | String? get title => _page.title; |
341 | |
342 | @override |
343 | bool get maintainState => _page.maintainState; |
344 | |
345 | @override |
346 | bool get fullscreenDialog => _page.fullscreenDialog; |
347 | |
348 | @override |
349 | String get debugLabel => '${super.debugLabel} (${_page.name} )'; |
350 | } |
351 | |
352 | /// A page that creates a cupertino style [PageRoute]. |
353 | /// |
354 | /// {@macro flutter.cupertino.cupertinoRouteTransitionMixin} |
355 | /// |
356 | /// By default, when a created modal route is replaced by another, the previous |
357 | /// route remains in memory. To free all the resources when this is not |
358 | /// necessary, set [maintainState] to false. |
359 | /// |
360 | /// The type `T` specifies the return type of the route which can be supplied as |
361 | /// the route is popped from the stack via [Navigator.transitionDelegate] by |
362 | /// providing the optional `result` argument to the |
363 | /// [RouteTransitionRecord.markForPop] in the [TransitionDelegate.resolve]. |
364 | /// |
365 | /// See also: |
366 | /// |
367 | /// * [CupertinoPageRoute], for a [PageRoute] version of this class. |
368 | class CupertinoPage<T> extends Page<T> { |
369 | /// Creates a cupertino page. |
370 | const CupertinoPage({ |
371 | required this.child, |
372 | this.maintainState = true, |
373 | this.title, |
374 | this.fullscreenDialog = false, |
375 | this.allowSnapshotting = true, |
376 | super.canPop, |
377 | super.onPopInvoked, |
378 | super.key, |
379 | super.name, |
380 | super.arguments, |
381 | super.restorationId, |
382 | }); |
383 | |
384 | /// The content to be shown in the [Route] created by this page. |
385 | final Widget child; |
386 | |
387 | /// {@macro flutter.cupertino.CupertinoRouteTransitionMixin.title} |
388 | final String? title; |
389 | |
390 | /// {@macro flutter.widgets.ModalRoute.maintainState} |
391 | final bool maintainState; |
392 | |
393 | /// {@macro flutter.widgets.PageRoute.fullscreenDialog} |
394 | final bool fullscreenDialog; |
395 | |
396 | /// {@macro flutter.widgets.TransitionRoute.allowSnapshotting} |
397 | final bool allowSnapshotting; |
398 | |
399 | @override |
400 | Route<T> createRoute(BuildContext context) { |
401 | return _PageBasedCupertinoPageRoute<T>(page: this, allowSnapshotting: allowSnapshotting); |
402 | } |
403 | } |
404 | |
405 | /// Provides an iOS-style page transition animation. |
406 | /// |
407 | /// The page slides in from the right and exits in reverse. It also shifts to the left in |
408 | /// a parallax motion when another page enters to cover it. |
409 | class CupertinoPageTransition extends StatefulWidget { |
410 | /// Creates an iOS-style page transition. |
411 | /// |
412 | const CupertinoPageTransition({ |
413 | super.key, |
414 | required this.primaryRouteAnimation, |
415 | required this.secondaryRouteAnimation, |
416 | required this.child, |
417 | required this.linearTransition, |
418 | }); |
419 | |
420 | /// The widget below this widget in the tree. |
421 | final Widget child; |
422 | |
423 | /// * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0 |
424 | /// when this screen is being pushed. |
425 | final Animation<double> primaryRouteAnimation; |
426 | |
427 | /// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0 |
428 | /// when another screen is being pushed on top of this one. |
429 | final Animation<double> secondaryRouteAnimation; |
430 | |
431 | /// * `linearTransition` is whether to perform the transitions linearly. |
432 | /// Used to precisely track back gesture drags. |
433 | final bool linearTransition; |
434 | |
435 | /// The Cupertino styled [DelegatedTransitionBuilder] provided to the previous |
436 | /// route. |
437 | /// |
438 | /// {@macro flutter.widgets.delegatedTransition} |
439 | static Widget? delegatedTransition(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, bool allowSnapshotting, Widget? child) { |
440 | final Animation<Offset> delegatedPositionAnimation = |
441 | CurvedAnimation( |
442 | parent: secondaryAnimation, |
443 | curve: Curves.linearToEaseOut, |
444 | reverseCurve: Curves.easeInToLinear, |
445 | ).drive(_kMiddleLeftTween); |
446 | assert(debugCheckHasDirectionality(context)); |
447 | final TextDirection textDirection = Directionality.of(context); |
448 | return SlideTransition( |
449 | position: delegatedPositionAnimation, |
450 | textDirection: textDirection, |
451 | transformHitTests: false, |
452 | child: child, |
453 | ); |
454 | } |
455 | |
456 | @override |
457 | State<CupertinoPageTransition> createState() => _CupertinoPageTransitionState(); |
458 | } |
459 | |
460 | class _CupertinoPageTransitionState extends State<CupertinoPageTransition> { |
461 | |
462 | |
463 | // When this page is coming in to cover another page. |
464 | late Animation<Offset> _primaryPositionAnimation; |
465 | // When this page is becoming covered by another page. |
466 | late Animation<Offset> _secondaryPositionAnimation; |
467 | // Shadow of page which is coming in to cover another page. |
468 | late Animation<Decoration> _primaryShadowAnimation; |
469 | // Curve of primary page which is coming in to cover another page. |
470 | CurvedAnimation? _primaryPositionCurve; |
471 | // Curve of secondary page which is becoming covered by another page. |
472 | CurvedAnimation? _secondaryPositionCurve; |
473 | // Curve of primary page's shadow. |
474 | CurvedAnimation? _primaryShadowCurve; |
475 | |
476 | @override |
477 | void initState() { |
478 | super.initState(); |
479 | _setupAnimation(); |
480 | } |
481 | |
482 | @override |
483 | void didUpdateWidget(covariant CupertinoPageTransition oldWidget) { |
484 | super.didUpdateWidget(oldWidget); |
485 | if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation |
486 | || oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation |
487 | || oldWidget.linearTransition != widget.linearTransition) { |
488 | _disposeCurve(); |
489 | _setupAnimation(); |
490 | } |
491 | } |
492 | |
493 | @override |
494 | void dispose() { |
495 | _disposeCurve(); |
496 | super.dispose(); |
497 | } |
498 | |
499 | void _disposeCurve() { |
500 | _primaryPositionCurve?.dispose(); |
501 | _secondaryPositionCurve?.dispose(); |
502 | _primaryShadowCurve?.dispose(); |
503 | _primaryPositionCurve = null; |
504 | _secondaryPositionCurve = null; |
505 | _primaryShadowCurve = null; |
506 | } |
507 | |
508 | void _setupAnimation() { |
509 | if (!widget.linearTransition) { |
510 | _primaryPositionCurve = CurvedAnimation( |
511 | parent: widget.primaryRouteAnimation, |
512 | curve: Curves.fastEaseInToSlowEaseOut, |
513 | reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, |
514 | ); |
515 | _secondaryPositionCurve = CurvedAnimation( |
516 | parent: widget.secondaryRouteAnimation, |
517 | curve: Curves.linearToEaseOut, |
518 | reverseCurve: Curves.easeInToLinear, |
519 | ); |
520 | _primaryShadowCurve = CurvedAnimation( |
521 | parent: widget.primaryRouteAnimation, |
522 | curve: Curves.linearToEaseOut, |
523 | ); |
524 | } |
525 | _primaryPositionAnimation = (_primaryPositionCurve ?? widget.primaryRouteAnimation) |
526 | .drive(_kRightMiddleTween); |
527 | _secondaryPositionAnimation = (_secondaryPositionCurve ?? widget.secondaryRouteAnimation) |
528 | .drive(_kMiddleLeftTween); |
529 | _primaryShadowAnimation = (_primaryShadowCurve ?? widget.primaryRouteAnimation) |
530 | .drive(_CupertinoEdgeShadowDecoration.kTween); |
531 | } |
532 | |
533 | @override |
534 | Widget build(BuildContext context) { |
535 | assert(debugCheckHasDirectionality(context)); |
536 | final TextDirection textDirection = Directionality.of(context); |
537 | return SlideTransition( |
538 | position: _secondaryPositionAnimation, |
539 | textDirection: textDirection, |
540 | transformHitTests: false, |
541 | child: SlideTransition( |
542 | position: _primaryPositionAnimation, |
543 | textDirection: textDirection, |
544 | child: DecoratedBoxTransition( |
545 | decoration: _primaryShadowAnimation, |
546 | child: widget.child, |
547 | ), |
548 | ), |
549 | ); |
550 | } |
551 | } |
552 | |
553 | /// An iOS-style transition used for summoning fullscreen dialogs. |
554 | /// |
555 | /// For example, used when creating a new calendar event by bringing in the next |
556 | /// screen from the bottom. |
557 | class CupertinoFullscreenDialogTransition extends StatefulWidget { |
558 | /// Creates an iOS-style transition used for summoning fullscreen dialogs. |
559 | /// |
560 | const CupertinoFullscreenDialogTransition({ |
561 | super.key, |
562 | required this.primaryRouteAnimation, |
563 | required this.secondaryRouteAnimation, |
564 | required this.child, |
565 | required this.linearTransition, |
566 | }); |
567 | |
568 | /// * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0 |
569 | /// when this screen is being pushed. |
570 | final Animation<double> primaryRouteAnimation; |
571 | |
572 | /// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0 |
573 | /// when another screen is being pushed on top of this one. |
574 | final Animation<double> secondaryRouteAnimation; |
575 | |
576 | /// * `linearTransition` is whether to perform the transitions linearly. |
577 | /// Used to precisely track back gesture drags. |
578 | final bool linearTransition; |
579 | |
580 | /// The widget below this widget in the tree. |
581 | final Widget child; |
582 | |
583 | @override |
584 | State<CupertinoFullscreenDialogTransition> createState() => _CupertinoFullscreenDialogTransitionState(); |
585 | } |
586 | |
587 | class _CupertinoFullscreenDialogTransitionState extends State<CupertinoFullscreenDialogTransition> { |
588 | /// When this page is coming in to cover another page. |
589 | late Animation<Offset> _primaryPositionAnimation; |
590 | /// When this page is becoming covered by another page. |
591 | late Animation<Offset> _secondaryPositionAnimation; |
592 | /// Curve of primary page which is coming in to cover another page. |
593 | CurvedAnimation? _primaryPositionCurve; |
594 | /// Curve of secondary page which is becoming covered by another page. |
595 | CurvedAnimation? _secondaryPositionCurve; |
596 | |
597 | @override |
598 | void initState() { |
599 | super.initState(); |
600 | _setupAnimation(); |
601 | } |
602 | |
603 | @override |
604 | void didUpdateWidget(covariant CupertinoFullscreenDialogTransition oldWidget) { |
605 | super.didUpdateWidget(oldWidget); |
606 | if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation || |
607 | oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation || |
608 | oldWidget.linearTransition != widget.linearTransition) { |
609 | _disposeCurve(); |
610 | _setupAnimation(); |
611 | } |
612 | } |
613 | |
614 | @override |
615 | void dispose() { |
616 | _disposeCurve(); |
617 | super.dispose(); |
618 | } |
619 | |
620 | void _disposeCurve() { |
621 | _primaryPositionCurve?.dispose(); |
622 | _secondaryPositionCurve?.dispose(); |
623 | _primaryPositionCurve = null; |
624 | _secondaryPositionCurve = null; |
625 | } |
626 | |
627 | void _setupAnimation() { |
628 | _primaryPositionAnimation = (_primaryPositionCurve = CurvedAnimation( |
629 | parent: widget.primaryRouteAnimation, |
630 | curve: Curves.linearToEaseOut, |
631 | // The curve must be flipped so that the reverse animation doesn't play |
632 | // an ease-in curve, which iOS does not use. |
633 | reverseCurve: Curves.linearToEaseOut.flipped, |
634 | )).drive(_kBottomUpTween); |
635 | _secondaryPositionAnimation = |
636 | (widget.linearTransition |
637 | ? widget.secondaryRouteAnimation |
638 | : _secondaryPositionCurve = CurvedAnimation( |
639 | parent: widget.secondaryRouteAnimation, |
640 | curve: Curves.linearToEaseOut, |
641 | reverseCurve: Curves.easeInToLinear, |
642 | ) |
643 | ).drive(_kMiddleLeftTween); |
644 | } |
645 | |
646 | |
647 | @override |
648 | Widget build(BuildContext context) { |
649 | assert(debugCheckHasDirectionality(context)); |
650 | final TextDirection textDirection = Directionality.of(context); |
651 | return SlideTransition( |
652 | position: _secondaryPositionAnimation, |
653 | textDirection: textDirection, |
654 | transformHitTests: false, |
655 | child: SlideTransition( |
656 | position: _primaryPositionAnimation, |
657 | child: widget.child, |
658 | ), |
659 | ); |
660 | } |
661 | } |
662 | |
663 | /// This is the widget side of [_CupertinoBackGestureController]. |
664 | /// |
665 | /// This widget provides a gesture recognizer which, when it determines the |
666 | /// route can be closed with a back gesture, creates the controller and |
667 | /// feeds it the input from the gesture recognizer. |
668 | /// |
669 | /// The gesture data is converted from absolute coordinates to logical |
670 | /// coordinates by this widget. |
671 | /// |
672 | /// The type `T` specifies the return type of the route with which this gesture |
673 | /// detector is associated. |
674 | class _CupertinoBackGestureDetector<T> extends StatefulWidget { |
675 | const _CupertinoBackGestureDetector({ |
676 | super.key, |
677 | required this.enabledCallback, |
678 | required this.onStartPopGesture, |
679 | required this.child, |
680 | }); |
681 | |
682 | final Widget child; |
683 | |
684 | final ValueGetter<bool> enabledCallback; |
685 | |
686 | final ValueGetter<_CupertinoBackGestureController<T>> onStartPopGesture; |
687 | |
688 | @override |
689 | _CupertinoBackGestureDetectorState<T> createState() => _CupertinoBackGestureDetectorState<T>(); |
690 | } |
691 | |
692 | class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureDetector<T>> { |
693 | _CupertinoBackGestureController<T>? _backGestureController; |
694 | |
695 | late HorizontalDragGestureRecognizer _recognizer; |
696 | |
697 | @override |
698 | void initState() { |
699 | super.initState(); |
700 | _recognizer = HorizontalDragGestureRecognizer(debugOwner: this) |
701 | ..onStart = _handleDragStart |
702 | ..onUpdate = _handleDragUpdate |
703 | ..onEnd = _handleDragEnd |
704 | ..onCancel = _handleDragCancel; |
705 | } |
706 | |
707 | @override |
708 | void dispose() { |
709 | _recognizer.dispose(); |
710 | |
711 | // If this is disposed during a drag, call navigator.didStopUserGesture. |
712 | if (_backGestureController != null) { |
713 | WidgetsBinding.instance.addPostFrameCallback((_) { |
714 | if (_backGestureController?.navigator.mounted ?? false) { |
715 | _backGestureController?.navigator.didStopUserGesture(); |
716 | } |
717 | _backGestureController = null; |
718 | }); |
719 | } |
720 | super.dispose(); |
721 | } |
722 | |
723 | void _handleDragStart(DragStartDetails details) { |
724 | assert(mounted); |
725 | assert(_backGestureController == null); |
726 | _backGestureController = widget.onStartPopGesture(); |
727 | } |
728 | |
729 | void _handleDragUpdate(DragUpdateDetails details) { |
730 | assert(mounted); |
731 | assert(_backGestureController != null); |
732 | _backGestureController!.dragUpdate(_convertToLogical(details.primaryDelta! / context.size!.width)); |
733 | } |
734 | |
735 | void _handleDragEnd(DragEndDetails details) { |
736 | assert(mounted); |
737 | assert(_backGestureController != null); |
738 | _backGestureController!.dragEnd(_convertToLogical(details.velocity.pixelsPerSecond.dx / context.size!.width)); |
739 | _backGestureController = null; |
740 | } |
741 | |
742 | void _handleDragCancel() { |
743 | assert(mounted); |
744 | // This can be called even if start is not called, paired with the "down" event |
745 | // that we don't consider here. |
746 | _backGestureController?.dragEnd(0.0); |
747 | _backGestureController = null; |
748 | } |
749 | |
750 | void _handlePointerDown(PointerDownEvent event) { |
751 | if (widget.enabledCallback()) { |
752 | _recognizer.addPointer(event); |
753 | } |
754 | } |
755 | |
756 | double _convertToLogical(double value) { |
757 | return switch (Directionality.of(context)) { |
758 | TextDirection.rtl => -value, |
759 | TextDirection.ltr => value, |
760 | }; |
761 | } |
762 | |
763 | @override |
764 | Widget build(BuildContext context) { |
765 | assert(debugCheckHasDirectionality(context)); |
766 | // For devices with notches, the drag area needs to be larger on the side |
767 | // that has the notch. |
768 | final double dragAreaWidth = switch (Directionality.of(context)) { |
769 | TextDirection.rtl => MediaQuery.paddingOf(context).right, |
770 | TextDirection.ltr => MediaQuery.paddingOf(context).left, |
771 | }; |
772 | return Stack( |
773 | fit: StackFit.passthrough, |
774 | children: <Widget>[ |
775 | widget.child, |
776 | PositionedDirectional( |
777 | start: 0.0, |
778 | width: max(dragAreaWidth, _kBackGestureWidth), |
779 | top: 0.0, |
780 | bottom: 0.0, |
781 | child: Listener( |
782 | onPointerDown: _handlePointerDown, |
783 | behavior: HitTestBehavior.translucent, |
784 | ), |
785 | ), |
786 | ], |
787 | ); |
788 | } |
789 | } |
790 | |
791 | /// A controller for an iOS-style back gesture. |
792 | /// |
793 | /// This is created by a [CupertinoPageRoute] in response from a gesture caught |
794 | /// by a [_CupertinoBackGestureDetector] widget, which then also feeds it input |
795 | /// from the gesture. It controls the animation controller owned by the route, |
796 | /// based on the input provided by the gesture detector. |
797 | /// |
798 | /// This class works entirely in logical coordinates (0.0 is new page dismissed, |
799 | /// 1.0 is new page on top). |
800 | /// |
801 | /// The type `T` specifies the return type of the route with which this gesture |
802 | /// detector controller is associated. |
803 | class _CupertinoBackGestureController<T> { |
804 | /// Creates a controller for an iOS-style back gesture. |
805 | _CupertinoBackGestureController({ |
806 | required this.navigator, |
807 | required this.controller, |
808 | required this.getIsActive, |
809 | required this.getIsCurrent, |
810 | }) { |
811 | navigator.didStartUserGesture(); |
812 | } |
813 | |
814 | final AnimationController controller; |
815 | final NavigatorState navigator; |
816 | final ValueGetter<bool> getIsActive; |
817 | final ValueGetter<bool> getIsCurrent; |
818 | |
819 | /// The drag gesture has changed by [delta]. The total range of the drag |
820 | /// should be 0.0 to 1.0. |
821 | void dragUpdate(double delta) { |
822 | controller.value -= delta; |
823 | } |
824 | |
825 | /// The drag gesture has ended with a horizontal motion of [velocity] as a |
826 | /// fraction of screen width per second. |
827 | void dragEnd(double velocity) { |
828 | // Fling in the appropriate direction. |
829 | // |
830 | // This curve has been determined through rigorously eyeballing native iOS |
831 | // animations. |
832 | const Curve animationCurve = Curves.fastEaseInToSlowEaseOut; |
833 | final bool isCurrent = getIsCurrent(); |
834 | final bool animateForward; |
835 | |
836 | if (!isCurrent) { |
837 | // If the page has already been navigated away from, then the animation |
838 | // direction depends on whether or not it's still in the navigation stack, |
839 | // regardless of velocity or drag position. For example, if a route is |
840 | // being slowly dragged back by just a few pixels, but then a programmatic |
841 | // pop occurs, the route should still be animated off the screen. |
842 | // See https://github.com/flutter/flutter/issues/141268. |
843 | animateForward = getIsActive(); |
844 | } else if (velocity.abs() >= _kMinFlingVelocity) { |
845 | // If the user releases the page before mid screen with sufficient velocity, |
846 | // or after mid screen, we should animate the page out. Otherwise, the page |
847 | // should be animated back in. |
848 | animateForward = velocity <= 0; |
849 | } else { |
850 | animateForward = controller.value > 0.5; |
851 | } |
852 | |
853 | if (animateForward) { |
854 | controller.animateTo(1.0, duration: _kDroppedSwipePageAnimationDuration, curve: animationCurve); |
855 | } else { |
856 | if (isCurrent) { |
857 | // This route is destined to pop at this point. Reuse navigator's pop. |
858 | navigator.pop(); |
859 | } |
860 | |
861 | // The popping may have finished inline if already at the target destination. |
862 | if (controller.isAnimating) { |
863 | controller.animateBack(0.0, duration: _kDroppedSwipePageAnimationDuration, curve: animationCurve); |
864 | } |
865 | } |
866 | |
867 | if (controller.isAnimating) { |
868 | // Keep the userGestureInProgress in true state so we don't change the |
869 | // curve of the page transition mid-flight since CupertinoPageTransition |
870 | // depends on userGestureInProgress. |
871 | late AnimationStatusListener animationStatusCallback; |
872 | animationStatusCallback = (AnimationStatus status) { |
873 | navigator.didStopUserGesture(); |
874 | controller.removeStatusListener(animationStatusCallback); |
875 | }; |
876 | controller.addStatusListener(animationStatusCallback); |
877 | } else { |
878 | navigator.didStopUserGesture(); |
879 | } |
880 | } |
881 | } |
882 | |
883 | // A custom [Decoration] used to paint an extra shadow on the start edge of the |
884 | // box it's decorating. It's like a [BoxDecoration] with only a gradient except |
885 | // it paints on the start side of the box instead of behind the box. |
886 | class _CupertinoEdgeShadowDecoration extends Decoration { |
887 | const _CupertinoEdgeShadowDecoration._([this._colors]); |
888 | |
889 | static DecorationTween kTween = DecorationTween( |
890 | begin: const _CupertinoEdgeShadowDecoration._(), // No decoration initially. |
891 | end: const _CupertinoEdgeShadowDecoration._( |
892 | // Eyeballed gradient used to mimic a drop shadow on the start side only. |
893 | <Color>[ |
894 | Color(0x04000000), |
895 | CupertinoColors.transparent, |
896 | ], |
897 | ), |
898 | ); |
899 | |
900 | // Colors used to paint a gradient at the start edge of the box it is |
901 | // decorating. |
902 | // |
903 | // The first color in the list is used at the start of the gradient, which |
904 | // is located at the start edge of the decorated box. |
905 | // |
906 | // If this is null, no shadow is drawn. |
907 | // |
908 | // The list must have at least two colors in it (otherwise it would not be a |
909 | // gradient). |
910 | final List<Color>? _colors; |
911 | |
912 | // Linearly interpolate between two edge shadow decorations decorations. |
913 | // |
914 | // The `t` argument represents position on the timeline, with 0.0 meaning |
915 | // that the interpolation has not started, returning `a` (or something |
916 | // equivalent to `a`), 1.0 meaning that the interpolation has finished, |
917 | // returning `b` (or something equivalent to `b`), and values in between |
918 | // meaning that the interpolation is at the relevant point on the timeline |
919 | // between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and |
920 | // 1.0, so negative values and values greater than 1.0 are valid (and can |
921 | // easily be generated by curves such as [Curves.elasticInOut]). |
922 | // |
923 | // Values for `t` are usually obtained from an [Animation |
924 | // an [AnimationController]. |
925 | // |
926 | // See also: |
927 | // |
928 | // * [Decoration.lerp]. |
929 | static _CupertinoEdgeShadowDecoration? lerp( |
930 | _CupertinoEdgeShadowDecoration? a, |
931 | _CupertinoEdgeShadowDecoration? b, |
932 | double t, |
933 | ) { |
934 | if (identical(a, b)) { |
935 | return a; |
936 | } |
937 | if (a == null) { |
938 | return b!._colors == null ? b : _CupertinoEdgeShadowDecoration._(b._colors!.map<Color>((Color color) => Color.lerp(null, color, t)!).toList()); |
939 | } |
940 | if (b == null) { |
941 | return a._colors == null ? a : _CupertinoEdgeShadowDecoration._(a._colors.map<Color>((Color color) => Color.lerp(null, color, 1.0 - t)!).toList()); |
942 | } |
943 | assert(b._colors != null || a._colors != null); |
944 | // If it ever becomes necessary, we could allow decorations with different |
945 | // length' here, similarly to how it is handled in [LinearGradient.lerp]. |
946 | assert(b._colors == null || a._colors == null || a._colors.length == b._colors.length); |
947 | return _CupertinoEdgeShadowDecoration._( |
948 | <Color>[ |
949 | for (int i = 0; i < b._colors!.length; i += 1) |
950 | Color.lerp(a._colors?[i], b._colors[i], t)!, |
951 | ], |
952 | ); |
953 | } |
954 | |
955 | @override |
956 | _CupertinoEdgeShadowDecoration lerpFrom(Decoration? a, double t) { |
957 | if (a is _CupertinoEdgeShadowDecoration) { |
958 | return _CupertinoEdgeShadowDecoration.lerp(a, this, t)!; |
959 | } |
960 | return _CupertinoEdgeShadowDecoration.lerp(null, this, t)!; |
961 | } |
962 | |
963 | @override |
964 | _CupertinoEdgeShadowDecoration lerpTo(Decoration? b, double t) { |
965 | if (b is _CupertinoEdgeShadowDecoration) { |
966 | return _CupertinoEdgeShadowDecoration.lerp(this, b, t)!; |
967 | } |
968 | return _CupertinoEdgeShadowDecoration.lerp(this, null, t)!; |
969 | } |
970 | |
971 | @override |
972 | _CupertinoEdgeShadowPainter createBoxPainter([ VoidCallback? onChanged ]) { |
973 | return _CupertinoEdgeShadowPainter(this, onChanged); |
974 | } |
975 | |
976 | @override |
977 | bool operator ==(Object other) { |
978 | if (other.runtimeType != runtimeType) { |
979 | return false; |
980 | } |
981 | return other is _CupertinoEdgeShadowDecoration |
982 | && other._colors == _colors; |
983 | } |
984 | |
985 | @override |
986 | int get hashCode => _colors.hashCode; |
987 | |
988 | @override |
989 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
990 | super.debugFillProperties(properties); |
991 | properties.add(IterableProperty<Color>('colors', _colors)); |
992 | } |
993 | } |
994 | |
995 | /// A [BoxPainter] used to draw the page transition shadow using gradients. |
996 | class _CupertinoEdgeShadowPainter extends BoxPainter { |
997 | _CupertinoEdgeShadowPainter( |
998 | this._decoration, |
999 | super.onChanged, |
1000 | ) : assert(_decoration._colors == null || _decoration._colors.length > 1); |
1001 | |
1002 | final _CupertinoEdgeShadowDecoration _decoration; |
1003 | |
1004 | @override |
1005 | void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { |
1006 | final List<Color>? colors = _decoration._colors; |
1007 | if (colors == null) { |
1008 | return; |
1009 | } |
1010 | |
1011 | // The following code simulates drawing a [LinearGradient] configured as |
1012 | // follows: |
1013 | // |
1014 | // LinearGradient( |
1015 | // begin: AlignmentDirectional(0.90, 0.0), // Spans 5% of the page. |
1016 | // colors: _decoration._colors, |
1017 | // ) |
1018 | // |
1019 | // A performance evaluation on Feb 8, 2021 showed, that drawing the gradient |
1020 | // manually as implemented below is more performant than relying on |
1021 | // [LinearGradient.createShader] because compiling that shader takes a long |
1022 | // time. On an iPhone XR, the implementation below reduced the worst frame |
1023 | // time for a cupertino page transition of a newly installed app from ~95ms |
1024 | // down to ~30ms, mainly because there's no longer a need to compile a |
1025 | // shader for the LinearGradient. |
1026 | // |
1027 | // The implementation below divides the width of the shadow into multiple |
1028 | // bands of equal width, one for each color interval defined by |
1029 | // `_decoration._colors`. Band x is filled with a gradient going from |
1030 | // `_decoration._colors[x]` to `_decoration._colors[x + 1]` by drawing a |
1031 | // bunch of 1px wide rects. The rects change their color by lerping between |
1032 | // the two colors that define the interval of the band. |
1033 | |
1034 | // Shadow spans 5% of the page. |
1035 | final double shadowWidth = 0.05 * configuration.size!.width; |
1036 | final double shadowHeight = configuration.size!.height; |
1037 | final double bandWidth = shadowWidth / (colors.length - 1); |
1038 | |
1039 | final TextDirection? textDirection = configuration.textDirection; |
1040 | assert(textDirection != null); |
1041 | final (double shadowDirection, double start) = switch (textDirection!) { |
1042 | TextDirection.rtl => (1, offset.dx + configuration.size!.width), |
1043 | TextDirection.ltr => (-1, offset.dx), |
1044 | }; |
1045 | |
1046 | int bandColorIndex = 0; |
1047 | for (int dx = 0; dx < shadowWidth; dx += 1) { |
1048 | if (dx ~/ bandWidth != bandColorIndex) { |
1049 | bandColorIndex += 1; |
1050 | } |
1051 | final Paint paint = Paint() |
1052 | ..color = Color.lerp(colors[bandColorIndex], colors[bandColorIndex + 1], (dx % bandWidth) / bandWidth)!; |
1053 | final double x = start + shadowDirection * dx; |
1054 | canvas.drawRect(Rect.fromLTWH(x - 1.0, offset.dy, 1.0, shadowHeight), paint); |
1055 | } |
1056 | } |
1057 | } |
1058 | |
1059 | /// A route that shows a modal iOS-style popup that slides up from the |
1060 | /// bottom of the screen. |
1061 | /// |
1062 | /// Such a popup is an alternative to a menu or a dialog and prevents the user |
1063 | /// from interacting with the rest of the app. |
1064 | /// |
1065 | /// It is used internally by [showCupertinoModalPopup] or can be directly pushed |
1066 | /// onto the [Navigator] stack to enable state restoration. See |
1067 | /// [showCupertinoModalPopup] for a state restoration app example. |
1068 | /// |
1069 | /// The `barrierColor` argument determines the [Color] of the barrier underneath |
1070 | /// the popup. When unspecified, the barrier color defaults to a light opacity |
1071 | /// black scrim based on iOS's dialog screens. To correctly have iOS resolve |
1072 | /// to the appropriate modal colors, pass in |
1073 | /// `CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context)`. |
1074 | /// |
1075 | /// The `barrierDismissible` argument determines whether clicking outside the |
1076 | /// popup results in dismissal. It is `true` by default. |
1077 | /// |
1078 | /// The `semanticsDismissible` argument is used to determine whether the |
1079 | /// semantics of the modal barrier are included in the semantics tree. |
1080 | /// |
1081 | /// The `routeSettings` argument is used to provide [RouteSettings] to the |
1082 | /// created Route. |
1083 | /// |
1084 | /// {@macro flutter.widgets.RawDialogRoute} |
1085 | /// |
1086 | /// See also: |
1087 | /// |
1088 | /// * [DisplayFeatureSubScreen], which documents the specifics of how |
1089 | /// [DisplayFeature]s can split the screen into sub-screens. |
1090 | /// * [CupertinoActionSheet], which is the widget usually returned by the |
1091 | /// `builder` argument. |
1092 | /// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/> |
1093 | class CupertinoModalPopupRoute<T> extends PopupRoute<T> { |
1094 | /// A route that shows a modal iOS-style popup that slides up from the |
1095 | /// bottom of the screen. |
1096 | CupertinoModalPopupRoute({ |
1097 | required this.builder, |
1098 | this.barrierLabel = 'Dismiss', |
1099 | this.barrierColor = kCupertinoModalBarrierColor, |
1100 | bool barrierDismissible = true, |
1101 | bool semanticsDismissible = false, |
1102 | super.filter, |
1103 | super.settings, |
1104 | super.requestFocus, |
1105 | this.anchorPoint, |
1106 | }) : _barrierDismissible = barrierDismissible, |
1107 | _semanticsDismissible = semanticsDismissible; |
1108 | |
1109 | /// A builder that builds the widget tree for the [CupertinoModalPopupRoute]. |
1110 | /// |
1111 | /// The [builder] argument typically builds a [CupertinoActionSheet] widget. |
1112 | /// |
1113 | /// Content below the widget is dimmed with a [ModalBarrier]. The widget built |
1114 | /// by the [builder] does not share a context with the route it was originally |
1115 | /// built from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the |
1116 | /// widget needs to update dynamically. |
1117 | final WidgetBuilder builder; |
1118 | |
1119 | final bool _barrierDismissible; |
1120 | |
1121 | final bool _semanticsDismissible; |
1122 | |
1123 | @override |
1124 | final String barrierLabel; |
1125 | |
1126 | @override |
1127 | final Color? barrierColor; |
1128 | |
1129 | @override |
1130 | bool get barrierDismissible => _barrierDismissible; |
1131 | |
1132 | @override |
1133 | bool get semanticsDismissible => _semanticsDismissible; |
1134 | |
1135 | @override |
1136 | Duration get transitionDuration => _kModalPopupTransitionDuration; |
1137 | |
1138 | CurvedAnimation? _animation; |
1139 | |
1140 | late Tween<Offset> _offsetTween; |
1141 | |
1142 | /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint} |
1143 | final Offset? anchorPoint; |
1144 | |
1145 | @override |
1146 | Animation<double> createAnimation() { |
1147 | assert(_animation == null); |
1148 | _animation = CurvedAnimation( |
1149 | parent: super.createAnimation(), |
1150 | |
1151 | // These curves were initially measured from native iOS horizontal page |
1152 | // route animations and seemed to be a good match here as well. |
1153 | curve: Curves.linearToEaseOut, |
1154 | reverseCurve: Curves.linearToEaseOut.flipped, |
1155 | ); |
1156 | _offsetTween = Tween<Offset>( |
1157 | begin: const Offset(0.0, 1.0), |
1158 | end: Offset.zero, |
1159 | ); |
1160 | return _animation!; |
1161 | } |
1162 | |
1163 | @override |
1164 | Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
1165 | return CupertinoUserInterfaceLevel( |
1166 | data: CupertinoUserInterfaceLevelData.elevated, |
1167 | child: DisplayFeatureSubScreen( |
1168 | anchorPoint: anchorPoint, |
1169 | child: Builder(builder: builder), |
1170 | ), |
1171 | ); |
1172 | } |
1173 | |
1174 | @override |
1175 | Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { |
1176 | return Align( |
1177 | alignment: Alignment.bottomCenter, |
1178 | child: FractionalTranslation( |
1179 | translation: _offsetTween.evaluate(_animation!), |
1180 | child: child, |
1181 | ), |
1182 | ); |
1183 | } |
1184 | |
1185 | @override |
1186 | void dispose() { |
1187 | _animation?.dispose(); |
1188 | super.dispose(); |
1189 | } |
1190 | } |
1191 | |
1192 | /// Shows a modal iOS-style popup that slides up from the bottom of the screen. |
1193 | /// |
1194 | /// Such a popup is an alternative to a menu or a dialog and prevents the user |
1195 | /// from interacting with the rest of the app. |
1196 | /// |
1197 | /// The `context` argument is used to look up the [Navigator] for the popup. |
1198 | /// It is only used when the method is called. Its corresponding widget can be |
1199 | /// safely removed from the tree before the popup is closed. |
1200 | /// |
1201 | /// The `barrierColor` argument determines the [Color] of the barrier underneath |
1202 | /// the popup. When unspecified, the barrier color defaults to a light opacity |
1203 | /// black scrim based on iOS's dialog screens. |
1204 | /// |
1205 | /// The `barrierDismissible` argument determines whether clicking outside the |
1206 | /// popup results in dismissal. It is `true` by default. |
1207 | /// |
1208 | /// The `useRootNavigator` argument is used to determine whether to push the |
1209 | /// popup to the [Navigator] furthest from or nearest to the given `context`. It |
1210 | /// is `true` by default. |
1211 | /// |
1212 | /// The `semanticsDismissible` argument is used to determine whether the |
1213 | /// semantics of the modal barrier are included in the semantics tree. |
1214 | /// |
1215 | /// The `routeSettings` argument is used to provide [RouteSettings] to the |
1216 | /// created Route. |
1217 | /// |
1218 | /// The `builder` argument typically builds a [CupertinoActionSheet] widget. |
1219 | /// Content below the widget is dimmed with a [ModalBarrier]. The widget built |
1220 | /// by the `builder` does not share a context with the location that |
1221 | /// [showCupertinoModalPopup] is originally called from. Use a |
1222 | /// [StatefulBuilder] or a custom [StatefulWidget] if the widget needs to |
1223 | /// update dynamically. |
1224 | /// |
1225 | /// {@macro flutter.widgets.RawDialogRoute} |
1226 | /// |
1227 | /// Returns a `Future` that resolves to the value that was passed to |
1228 | /// [Navigator.pop] when the popup was closed. |
1229 | /// |
1230 | /// ### State Restoration in Modals |
1231 | /// |
1232 | /// Using this method will not enable state restoration for the modal. In order |
1233 | /// to enable state restoration for a modal, use [Navigator.restorablePush] |
1234 | /// or [Navigator.restorablePushNamed] with [CupertinoModalPopupRoute]. |
1235 | /// |
1236 | /// For more information about state restoration, see [RestorationManager]. |
1237 | /// |
1238 | /// {@tool dartpad} |
1239 | /// This sample demonstrates how to create a restorable Cupertino modal route. |
1240 | /// This is accomplished by enabling state restoration by specifying |
1241 | /// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to |
1242 | /// push [CupertinoModalPopupRoute] when the [CupertinoButton] is tapped. |
1243 | /// |
1244 | /// {@macro flutter.widgets.RestorationManager} |
1245 | /// |
1246 | /// ** See code in examples/api/lib/cupertino/route/show_cupertino_modal_popup.0.dart ** |
1247 | /// {@end-tool} |
1248 | /// |
1249 | /// See also: |
1250 | /// |
1251 | /// * [DisplayFeatureSubScreen], which documents the specifics of how |
1252 | /// [DisplayFeature]s can split the screen into sub-screens. |
1253 | /// * [CupertinoActionSheet], which is the widget usually returned by the |
1254 | /// `builder` argument to [showCupertinoModalPopup]. |
1255 | /// * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/> |
1256 | Future<T?> showCupertinoModalPopup<T>({ |
1257 | required BuildContext context, |
1258 | required WidgetBuilder builder, |
1259 | ImageFilter? filter, |
1260 | Color barrierColor = kCupertinoModalBarrierColor, |
1261 | bool barrierDismissible = true, |
1262 | bool useRootNavigator = true, |
1263 | bool semanticsDismissible = false, |
1264 | RouteSettings? routeSettings, |
1265 | Offset? anchorPoint, |
1266 | }) { |
1267 | return Navigator.of(context, rootNavigator: useRootNavigator).push( |
1268 | CupertinoModalPopupRoute<T>( |
1269 | builder: builder, |
1270 | filter: filter, |
1271 | barrierColor: CupertinoDynamicColor.resolve(barrierColor, context), |
1272 | barrierDismissible: barrierDismissible, |
1273 | semanticsDismissible: semanticsDismissible, |
1274 | settings: routeSettings, |
1275 | anchorPoint: anchorPoint, |
1276 | ), |
1277 | ); |
1278 | } |
1279 | |
1280 | // The curve and initial scale values were mostly eyeballed from iOS, however |
1281 | // they reuse the same animation curve that was modeled after native page |
1282 | // transitions. |
1283 | final Animatable<double> _dialogScaleTween = Tween<double>(begin: 1.3, end: 1.0) |
1284 | .chain(CurveTween(curve: Curves.linearToEaseOut)); |
1285 | |
1286 | Widget _buildCupertinoDialogTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { |
1287 | return child; |
1288 | } |
1289 | |
1290 | /// Displays an iOS-style dialog above the current contents of the app, with |
1291 | /// iOS-style entrance and exit animations, modal barrier color, and modal |
1292 | /// barrier behavior (by default, the dialog is not dismissible with a tap on |
1293 | /// the barrier). |
1294 | /// |
1295 | /// This function takes a `builder` which typically builds a [CupertinoAlertDialog] |
1296 | /// widget. Content below the dialog is dimmed with a [ModalBarrier]. The widget |
1297 | /// returned by the `builder` does not share a context with the location that |
1298 | /// [showCupertinoDialog] is originally called from. Use a [StatefulBuilder] or |
1299 | /// a custom [StatefulWidget] if the dialog needs to update dynamically. |
1300 | /// |
1301 | /// The `context` argument is used to look up the [Navigator] for the dialog. |
1302 | /// It is only used when the method is called. Its corresponding widget can |
1303 | /// be safely removed from the tree before the dialog is closed. |
1304 | /// |
1305 | /// The `useRootNavigator` argument is used to determine whether to push the |
1306 | /// dialog to the [Navigator] furthest from or nearest to the given `context`. |
1307 | /// By default, `useRootNavigator` is `true` and the dialog route created by |
1308 | /// this method is pushed to the root navigator. |
1309 | /// |
1310 | /// {@macro flutter.widgets.RawDialogRoute} |
1311 | /// |
1312 | /// If the application has multiple [Navigator] objects, it may be necessary to |
1313 | /// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the |
1314 | /// dialog rather than just `Navigator.pop(context, result)`. |
1315 | /// |
1316 | /// Returns a [Future] that resolves to the value (if any) that was passed to |
1317 | /// [Navigator.pop] when the dialog was closed. |
1318 | /// |
1319 | /// ### State Restoration in Dialogs |
1320 | /// |
1321 | /// Using this method will not enable state restoration for the dialog. In order |
1322 | /// to enable state restoration for a dialog, use [Navigator.restorablePush] |
1323 | /// or [Navigator.restorablePushNamed] with [CupertinoDialogRoute]. |
1324 | /// |
1325 | /// For more information about state restoration, see [RestorationManager]. |
1326 | /// |
1327 | /// {@tool dartpad} |
1328 | /// This sample demonstrates how to create a restorable Cupertino dialog. This is |
1329 | /// accomplished by enabling state restoration by specifying |
1330 | /// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to |
1331 | /// push [CupertinoDialogRoute] when the [CupertinoButton] is tapped. |
1332 | /// |
1333 | /// {@macro flutter.widgets.RestorationManager} |
1334 | /// |
1335 | /// ** See code in examples/api/lib/cupertino/route/show_cupertino_dialog.0.dart ** |
1336 | /// {@end-tool} |
1337 | /// |
1338 | /// See also: |
1339 | /// |
1340 | /// * [CupertinoAlertDialog], an iOS-style alert dialog. |
1341 | /// * [showDialog], which displays a Material-style dialog. |
1342 | /// * [showGeneralDialog], which allows for customization of the dialog popup. |
1343 | /// * [DisplayFeatureSubScreen], which documents the specifics of how |
1344 | /// [DisplayFeature]s can split the screen into sub-screens. |
1345 | /// * <https://developer.apple.com/design/human-interface-guidelines/alerts/> |
1346 | Future<T?> showCupertinoDialog<T>({ |
1347 | required BuildContext context, |
1348 | required WidgetBuilder builder, |
1349 | String? barrierLabel, |
1350 | bool useRootNavigator = true, |
1351 | bool barrierDismissible = false, |
1352 | RouteSettings? routeSettings, |
1353 | Offset? anchorPoint, |
1354 | }) { |
1355 | |
1356 | return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(CupertinoDialogRoute<T>( |
1357 | builder: builder, |
1358 | context: context, |
1359 | barrierDismissible: barrierDismissible, |
1360 | barrierLabel: barrierLabel, |
1361 | barrierColor: CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context), |
1362 | settings: routeSettings, |
1363 | anchorPoint: anchorPoint, |
1364 | )); |
1365 | } |
1366 | |
1367 | /// A dialog route that shows an iOS-style dialog. |
1368 | /// |
1369 | /// It is used internally by [showCupertinoDialog] or can be directly pushed |
1370 | /// onto the [Navigator] stack to enable state restoration. See |
1371 | /// [showCupertinoDialog] for a state restoration app example. |
1372 | /// |
1373 | /// This function takes a `builder` which typically builds a [Dialog] widget. |
1374 | /// Content below the dialog is dimmed with a [ModalBarrier]. The widget |
1375 | /// returned by the `builder` does not share a context with the location that |
1376 | /// `showDialog` is originally called from. Use a [StatefulBuilder] or a |
1377 | /// custom [StatefulWidget] if the dialog needs to update dynamically. |
1378 | /// |
1379 | /// The `context` argument is used to look up |
1380 | /// [CupertinoLocalizations.modalBarrierDismissLabel], which provides the |
1381 | /// modal with a localized accessibility label that will be used for the |
1382 | /// modal's barrier. However, a custom `barrierLabel` can be passed in as well. |
1383 | /// |
1384 | /// The `barrierDismissible` argument is used to indicate whether tapping on the |
1385 | /// barrier will dismiss the dialog. It is `true` by default and cannot be `null`. |
1386 | /// |
1387 | /// The `barrierColor` argument is used to specify the color of the modal |
1388 | /// barrier that darkens everything below the dialog. If `null`, then |
1389 | /// [CupertinoDynamicColor.resolve] is used to compute the modal color. |
1390 | /// |
1391 | /// The `settings` argument define the settings for this route. See |
1392 | /// [RouteSettings] for details. |
1393 | /// |
1394 | /// {@macro flutter.widgets.RawDialogRoute} |
1395 | /// |
1396 | /// See also: |
1397 | /// |
1398 | /// * [showCupertinoDialog], which is a way to display |
1399 | /// an iOS-style dialog. |
1400 | /// * [showGeneralDialog], which allows for customization of the dialog popup. |
1401 | /// * [showDialog], which displays a Material dialog. |
1402 | /// * [DisplayFeatureSubScreen], which documents the specifics of how |
1403 | /// [DisplayFeature]s can split the screen into sub-screens. |
1404 | class CupertinoDialogRoute<T> extends RawDialogRoute<T> { |
1405 | /// A dialog route that shows an iOS-style dialog. |
1406 | CupertinoDialogRoute({ |
1407 | required WidgetBuilder builder, |
1408 | required BuildContext context, |
1409 | super.barrierDismissible, |
1410 | Color? barrierColor, |
1411 | String? barrierLabel, |
1412 | // This transition duration was eyeballed comparing with iOS |
1413 | super.transitionDuration = const Duration(milliseconds: 250), |
1414 | this.transitionBuilder, |
1415 | super.settings, |
1416 | super.requestFocus, |
1417 | super.anchorPoint, |
1418 | }) : super( |
1419 | pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
1420 | return builder(context); |
1421 | }, |
1422 | transitionBuilder: transitionBuilder ?? _buildCupertinoDialogTransitions, |
1423 | barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel, |
1424 | barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(kCupertinoModalBarrierColor, context), |
1425 | ); |
1426 | |
1427 | /// Custom transition builder |
1428 | RouteTransitionsBuilder? transitionBuilder; |
1429 | |
1430 | CurvedAnimation? _fadeAnimation; |
1431 | |
1432 | @override |
1433 | Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { |
1434 | |
1435 | if (transitionBuilder != null) { |
1436 | return super.buildTransitions(context, animation, secondaryAnimation, child); |
1437 | } |
1438 | |
1439 | if (_fadeAnimation?.parent != animation) { |
1440 | _fadeAnimation?.dispose(); |
1441 | _fadeAnimation = CurvedAnimation( |
1442 | parent: animation, |
1443 | curve: Curves.easeInOut, |
1444 | ); |
1445 | } |
1446 | |
1447 | final CurvedAnimation fadeAnimation = _fadeAnimation!; |
1448 | |
1449 | if (animation.status == AnimationStatus.reverse) { |
1450 | return FadeTransition( |
1451 | opacity: fadeAnimation, |
1452 | child: super.buildTransitions(context, animation, secondaryAnimation, child), |
1453 | ); |
1454 | } |
1455 | return FadeTransition( |
1456 | opacity: fadeAnimation, |
1457 | child: ScaleTransition( |
1458 | scale: animation.drive(_dialogScaleTween), |
1459 | child: super.buildTransitions(context, animation, secondaryAnimation, child), |
1460 | ), |
1461 | ); |
1462 | } |
1463 | |
1464 | @override |
1465 | void dispose() { |
1466 | _fadeAnimation?.dispose(); |
1467 | super.dispose(); |
1468 | } |
1469 | } |
1470 |
Definitions
- _kBackGestureWidth
- _kMinFlingVelocity
- _kDroppedSwipePageAnimationDuration
- _kCupertinoPageTransitionBarrierColor
- kCupertinoModalBarrierColor
- _kModalPopupTransitionDuration
- _kRightMiddleTween
- _kMiddleLeftTween
- _kBottomUpTween
- CupertinoRouteTransitionMixin
- buildContent
- title
- previousTitle
- dispose
- didChangePrevious
- transitionDuration
- barrierColor
- barrierLabel
- canTransitionTo
- buildPage
- _startPopGesture
- buildPageTransitions
- buildTransitions
- CupertinoPageRoute
- CupertinoPageRoute
- delegatedTransition
- buildContent
- debugLabel
- _PageBasedCupertinoPageRoute
- _PageBasedCupertinoPageRoute
- delegatedTransition
- _page
- buildContent
- title
- maintainState
- fullscreenDialog
- debugLabel
- CupertinoPage
- CupertinoPage
- createRoute
- CupertinoPageTransition
- CupertinoPageTransition
- delegatedTransition
- createState
- _CupertinoPageTransitionState
- initState
- didUpdateWidget
- dispose
- _disposeCurve
- _setupAnimation
- build
- CupertinoFullscreenDialogTransition
- CupertinoFullscreenDialogTransition
- createState
- _CupertinoFullscreenDialogTransitionState
- initState
- didUpdateWidget
- dispose
- _disposeCurve
- _setupAnimation
- build
- _CupertinoBackGestureDetector
- _CupertinoBackGestureDetector
- createState
- _CupertinoBackGestureDetectorState
- initState
- dispose
- _handleDragStart
- _handleDragUpdate
- _handleDragEnd
- _handleDragCancel
- _handlePointerDown
- _convertToLogical
- build
- _CupertinoBackGestureController
- _CupertinoBackGestureController
- dragUpdate
- dragEnd
- _CupertinoEdgeShadowDecoration
- _
- lerp
- lerpFrom
- lerpTo
- createBoxPainter
- ==
- hashCode
- debugFillProperties
- _CupertinoEdgeShadowPainter
- _CupertinoEdgeShadowPainter
- paint
- CupertinoModalPopupRoute
- CupertinoModalPopupRoute
- barrierDismissible
- semanticsDismissible
- transitionDuration
- createAnimation
- buildPage
- buildTransitions
- dispose
- showCupertinoModalPopup
- _dialogScaleTween
- _buildCupertinoDialogTransitions
- showCupertinoDialog
- CupertinoDialogRoute
- CupertinoDialogRoute
- buildTransitions
Learn more about Flutter for embedded and desktop on industrialflutter.com