| 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 'scroll_view.dart'; |
| 6 | library; |
| 7 | |
| 8 | import 'package:flutter/gestures.dart'; |
| 9 | |
| 10 | import 'automatic_keep_alive.dart'; |
| 11 | import 'basic.dart'; |
| 12 | import 'debug.dart'; |
| 13 | import 'framework.dart'; |
| 14 | import 'gesture_detector.dart'; |
| 15 | import 'ticker_provider.dart'; |
| 16 | import 'transitions.dart'; |
| 17 | |
| 18 | const Curve _kResizeTimeCurve = Interval(0.4, 1.0, curve: Curves.ease); |
| 19 | const double _kMinFlingVelocity = 700.0; |
| 20 | const double _kMinFlingVelocityDelta = 400.0; |
| 21 | const double _kFlingVelocityScale = 1.0 / 300.0; |
| 22 | const double _kDismissThreshold = 0.4; |
| 23 | |
| 24 | /// Signature used by [Dismissible] to indicate that it has been dismissed in |
| 25 | /// the given `direction`. |
| 26 | /// |
| 27 | /// Used by [Dismissible.onDismissed]. |
| 28 | typedef DismissDirectionCallback = void Function(DismissDirection direction); |
| 29 | |
| 30 | /// Signature used by [Dismissible] to give the application an opportunity to |
| 31 | /// confirm or veto a dismiss gesture. |
| 32 | /// |
| 33 | /// Used by [Dismissible.confirmDismiss]. |
| 34 | typedef ConfirmDismissCallback = Future<bool?> Function(DismissDirection direction); |
| 35 | |
| 36 | /// Signature used by [Dismissible] to indicate that the dismissible has been dragged. |
| 37 | /// |
| 38 | /// Used by [Dismissible.onUpdate]. |
| 39 | typedef DismissUpdateCallback = void Function(DismissUpdateDetails details); |
| 40 | |
| 41 | /// The direction in which a [Dismissible] can be dismissed. |
| 42 | enum DismissDirection { |
| 43 | /// The [Dismissible] can be dismissed by dragging either up or down. |
| 44 | vertical, |
| 45 | |
| 46 | /// The [Dismissible] can be dismissed by dragging either left or right. |
| 47 | horizontal, |
| 48 | |
| 49 | /// The [Dismissible] can be dismissed by dragging in the reverse of the |
| 50 | /// reading direction (e.g., from right to left in left-to-right languages). |
| 51 | endToStart, |
| 52 | |
| 53 | /// The [Dismissible] can be dismissed by dragging in the reading direction |
| 54 | /// (e.g., from left to right in left-to-right languages). |
| 55 | startToEnd, |
| 56 | |
| 57 | /// The [Dismissible] can be dismissed by dragging up only. |
| 58 | up, |
| 59 | |
| 60 | /// The [Dismissible] can be dismissed by dragging down only. |
| 61 | down, |
| 62 | |
| 63 | /// The [Dismissible] cannot be dismissed by dragging. |
| 64 | none, |
| 65 | } |
| 66 | |
| 67 | /// A widget that can be dismissed by dragging in the indicated [direction]. |
| 68 | /// |
| 69 | /// Dragging or flinging this widget in the [DismissDirection] causes the child |
| 70 | /// to slide out of view. Following the slide animation, if [resizeDuration] is |
| 71 | /// non-null, the Dismissible widget animates its height (or width, whichever is |
| 72 | /// perpendicular to the dismiss direction) to zero over the [resizeDuration]. |
| 73 | /// |
| 74 | /// {@youtube 560 315 https://www.youtube.com/watch?v=iEMgjrfuc58} |
| 75 | /// |
| 76 | /// {@tool dartpad} |
| 77 | /// This sample shows how you can use the [Dismissible] widget to |
| 78 | /// remove list items using swipe gestures. Swipe any of the list |
| 79 | /// tiles to the left or right to dismiss them from the [ListView]. |
| 80 | /// |
| 81 | /// ** See code in examples/api/lib/widgets/dismissible/dismissible.0.dart ** |
| 82 | /// {@end-tool} |
| 83 | /// |
| 84 | /// Backgrounds can be used to implement the "leave-behind" idiom. If a background |
| 85 | /// is specified it is stacked behind the Dismissible's child and is exposed when |
| 86 | /// the child moves. |
| 87 | /// |
| 88 | /// The widget calls the [onDismissed] callback either after its size has |
| 89 | /// collapsed to zero (if [resizeDuration] is non-null) or immediately after |
| 90 | /// the slide animation (if [resizeDuration] is null). If the Dismissible is a |
| 91 | /// list item, it must have a key that distinguishes it from the other items and |
| 92 | /// its [onDismissed] callback must remove the item from the list. |
| 93 | class Dismissible extends StatefulWidget { |
| 94 | /// Creates a widget that can be dismissed. |
| 95 | /// |
| 96 | /// The [key] argument is required because [Dismissible]s are commonly used in |
| 97 | /// lists and removed from the list when dismissed. Without keys, the default |
| 98 | /// behavior is to sync widgets based on their index in the list, which means |
| 99 | /// the item after the dismissed item would be synced with the state of the |
| 100 | /// dismissed item. Using keys causes the widgets to sync according to their |
| 101 | /// keys and avoids this pitfall. |
| 102 | const Dismissible({ |
| 103 | required Key super.key, |
| 104 | required this.child, |
| 105 | this.background, |
| 106 | this.secondaryBackground, |
| 107 | this.confirmDismiss, |
| 108 | this.onResize, |
| 109 | this.onUpdate, |
| 110 | this.onDismissed, |
| 111 | this.direction = DismissDirection.horizontal, |
| 112 | this.resizeDuration = const Duration(milliseconds: 300), |
| 113 | this.dismissThresholds = const <DismissDirection, double>{}, |
| 114 | this.movementDuration = const Duration(milliseconds: 200), |
| 115 | this.crossAxisEndOffset = 0.0, |
| 116 | this.dragStartBehavior = DragStartBehavior.start, |
| 117 | this.behavior = HitTestBehavior.opaque, |
| 118 | }) : assert(secondaryBackground == null || background != null); |
| 119 | |
| 120 | /// The widget below this widget in the tree. |
| 121 | /// |
| 122 | /// {@macro flutter.widgets.ProxyWidget.child} |
| 123 | final Widget child; |
| 124 | |
| 125 | /// A widget that is stacked behind the child. If secondaryBackground is also |
| 126 | /// specified then this widget only appears when the child has been dragged |
| 127 | /// down or to the right. |
| 128 | final Widget? background; |
| 129 | |
| 130 | /// A widget that is stacked behind the child and is exposed when the child |
| 131 | /// has been dragged up or to the left. It may only be specified when background |
| 132 | /// has also been specified. |
| 133 | final Widget? secondaryBackground; |
| 134 | |
| 135 | /// Gives the app an opportunity to confirm or veto a pending dismissal. |
| 136 | /// |
| 137 | /// The widget cannot be dragged again until the returned future resolves. |
| 138 | /// |
| 139 | /// If the returned `Future<bool>` completes true, then this widget will be |
| 140 | /// dismissed, otherwise it will be moved back to its original location. |
| 141 | /// |
| 142 | /// If the returned `Future<bool?>` completes to false or null the [onResize] |
| 143 | /// and [onDismissed] callbacks will not run. |
| 144 | final ConfirmDismissCallback? confirmDismiss; |
| 145 | |
| 146 | /// Called when the widget changes size (i.e., when contracting before being dismissed). |
| 147 | final VoidCallback? onResize; |
| 148 | |
| 149 | /// Called when the widget has been dismissed, after finishing resizing. |
| 150 | final DismissDirectionCallback? onDismissed; |
| 151 | |
| 152 | /// The direction in which the widget can be dismissed. |
| 153 | final DismissDirection direction; |
| 154 | |
| 155 | /// The amount of time the widget will spend contracting before [onDismissed] is called. |
| 156 | /// |
| 157 | /// If null, the widget will not contract and [onDismissed] will be called |
| 158 | /// immediately after the widget is dismissed. |
| 159 | final Duration? resizeDuration; |
| 160 | |
| 161 | /// The offset threshold the item has to be dragged in order to be considered |
| 162 | /// dismissed. |
| 163 | /// |
| 164 | /// Represented as a fraction, e.g. if it is 0.4 (the default), then the item |
| 165 | /// has to be dragged at least 40% towards one direction to be considered |
| 166 | /// dismissed. Clients can define different thresholds for each dismiss |
| 167 | /// direction. |
| 168 | /// |
| 169 | /// Flinging is treated as being equivalent to dragging almost to 1.0, so |
| 170 | /// flinging can dismiss an item past any threshold less than 1.0. |
| 171 | /// |
| 172 | /// Setting a threshold of 1.0 (or greater) prevents a drag in the given |
| 173 | /// [DismissDirection] even if it would be allowed by the [direction] |
| 174 | /// property. |
| 175 | /// |
| 176 | /// See also: |
| 177 | /// |
| 178 | /// * [direction], which controls the directions in which the items can |
| 179 | /// be dismissed. |
| 180 | final Map<DismissDirection, double> dismissThresholds; |
| 181 | |
| 182 | /// Defines the duration for card to dismiss or to come back to original position if not dismissed. |
| 183 | final Duration movementDuration; |
| 184 | |
| 185 | /// Defines the end offset across the main axis after the card is dismissed. |
| 186 | /// |
| 187 | /// If non-zero value is given then widget moves in cross direction depending on whether |
| 188 | /// it is positive or negative. |
| 189 | final double crossAxisEndOffset; |
| 190 | |
| 191 | /// Determines the way that drag start behavior is handled. |
| 192 | /// |
| 193 | /// If set to [DragStartBehavior.start], the drag gesture used to dismiss a |
| 194 | /// dismissible will begin at the position where the drag gesture won the arena. |
| 195 | /// If set to [DragStartBehavior.down] it will begin at the position where |
| 196 | /// a down event is first detected. |
| 197 | /// |
| 198 | /// In general, setting this to [DragStartBehavior.start] will make drag |
| 199 | /// animation smoother and setting it to [DragStartBehavior.down] will make |
| 200 | /// drag behavior feel slightly more reactive. |
| 201 | /// |
| 202 | /// By default, the drag start behavior is [DragStartBehavior.start]. |
| 203 | /// |
| 204 | /// See also: |
| 205 | /// |
| 206 | /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. |
| 207 | final DragStartBehavior dragStartBehavior; |
| 208 | |
| 209 | /// How to behave during hit tests. |
| 210 | /// |
| 211 | /// This defaults to [HitTestBehavior.opaque]. |
| 212 | final HitTestBehavior behavior; |
| 213 | |
| 214 | /// Called when the dismissible widget has been dragged. |
| 215 | /// |
| 216 | /// If [onUpdate] is not null, then it will be invoked for every pointer event |
| 217 | /// to dispatch the latest state of the drag. For example, this callback |
| 218 | /// can be used to for example change the color of the background widget |
| 219 | /// depending on whether the dismiss threshold is currently reached. |
| 220 | final DismissUpdateCallback? onUpdate; |
| 221 | |
| 222 | @override |
| 223 | State<Dismissible> createState() => _DismissibleState(); |
| 224 | } |
| 225 | |
| 226 | /// Details for [DismissUpdateCallback]. |
| 227 | /// |
| 228 | /// See also: |
| 229 | /// |
| 230 | /// * [Dismissible.onUpdate], which receives this information. |
| 231 | class DismissUpdateDetails { |
| 232 | /// Create a new instance of [DismissUpdateDetails]. |
| 233 | DismissUpdateDetails({ |
| 234 | this.direction = DismissDirection.horizontal, |
| 235 | this.reached = false, |
| 236 | this.previousReached = false, |
| 237 | this.progress = 0.0, |
| 238 | }); |
| 239 | |
| 240 | /// The direction that the dismissible is being dragged. |
| 241 | final DismissDirection direction; |
| 242 | |
| 243 | /// Whether the dismiss threshold is currently reached. |
| 244 | final bool reached; |
| 245 | |
| 246 | /// Whether the dismiss threshold was reached the last time this callback was invoked. |
| 247 | /// |
| 248 | /// This can be used in conjunction with [DismissUpdateDetails.reached] to catch the moment |
| 249 | /// that the [Dismissible] is dragged across the threshold. |
| 250 | final bool previousReached; |
| 251 | |
| 252 | /// The offset ratio of the dismissible in its parent container. |
| 253 | /// |
| 254 | /// A value of 0.0 represents the normal position and 1.0 means the child is |
| 255 | /// completely outside its parent. |
| 256 | /// |
| 257 | /// This can be used to synchronize other elements to what the dismissible is doing on screen, |
| 258 | /// e.g. using this value to set the opacity thereby fading dismissible as it's dragged offscreen. |
| 259 | final double progress; |
| 260 | } |
| 261 | |
| 262 | class _DismissibleClipper extends CustomClipper<Rect> { |
| 263 | _DismissibleClipper({required this.axis, required this.moveAnimation}) |
| 264 | : super(reclip: moveAnimation); |
| 265 | |
| 266 | final Axis axis; |
| 267 | final Animation<Offset> moveAnimation; |
| 268 | |
| 269 | @override |
| 270 | Rect getClip(Size size) { |
| 271 | switch (axis) { |
| 272 | case Axis.horizontal: |
| 273 | final double offset = moveAnimation.value.dx * size.width; |
| 274 | if (offset < 0) { |
| 275 | return Rect.fromLTRB(size.width + offset, 0.0, size.width, size.height); |
| 276 | } |
| 277 | return Rect.fromLTRB(0.0, 0.0, offset, size.height); |
| 278 | case Axis.vertical: |
| 279 | final double offset = moveAnimation.value.dy * size.height; |
| 280 | if (offset < 0) { |
| 281 | return Rect.fromLTRB(0.0, size.height + offset, size.width, size.height); |
| 282 | } |
| 283 | return Rect.fromLTRB(0.0, 0.0, size.width, offset); |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | @override |
| 288 | Rect getApproximateClipRect(Size size) => getClip(size); |
| 289 | |
| 290 | @override |
| 291 | bool shouldReclip(_DismissibleClipper oldClipper) { |
| 292 | return oldClipper.axis != axis || oldClipper.moveAnimation.value != moveAnimation.value; |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | enum _FlingGestureKind { none, forward, reverse } |
| 297 | |
| 298 | class _DismissibleState extends State<Dismissible> |
| 299 | with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { |
| 300 | @override |
| 301 | void initState() { |
| 302 | super.initState(); |
| 303 | _moveController |
| 304 | ..addStatusListener(_handleDismissStatusChanged) |
| 305 | ..addListener(_handleDismissUpdateValueChanged); |
| 306 | _updateMoveAnimation(); |
| 307 | } |
| 308 | |
| 309 | late final AnimationController _moveController = AnimationController( |
| 310 | duration: widget.movementDuration, |
| 311 | vsync: this, |
| 312 | ); |
| 313 | late Animation<Offset> _moveAnimation; |
| 314 | |
| 315 | AnimationController? _resizeController; |
| 316 | Animation<double>? _resizeAnimation; |
| 317 | |
| 318 | double _dragExtent = 0.0; |
| 319 | bool _confirming = false; |
| 320 | bool _dragUnderway = false; |
| 321 | Size? _sizePriorToCollapse; |
| 322 | bool _dismissThresholdReached = false; |
| 323 | |
| 324 | final GlobalKey _contentKey = GlobalKey(); |
| 325 | |
| 326 | @override |
| 327 | bool get wantKeepAlive => |
| 328 | _moveController.isAnimating || (_resizeController?.isAnimating ?? false); |
| 329 | |
| 330 | @override |
| 331 | void dispose() { |
| 332 | _moveController.dispose(); |
| 333 | _resizeController?.dispose(); |
| 334 | super.dispose(); |
| 335 | } |
| 336 | |
| 337 | bool get _directionIsXAxis { |
| 338 | return widget.direction == DismissDirection.horizontal || |
| 339 | widget.direction == DismissDirection.endToStart || |
| 340 | widget.direction == DismissDirection.startToEnd; |
| 341 | } |
| 342 | |
| 343 | DismissDirection _extentToDirection(double extent) { |
| 344 | if (extent == 0.0) { |
| 345 | return DismissDirection.none; |
| 346 | } |
| 347 | if (_directionIsXAxis) { |
| 348 | return switch (Directionality.of(context)) { |
| 349 | TextDirection.rtl when extent < 0 => DismissDirection.startToEnd, |
| 350 | TextDirection.ltr when extent > 0 => DismissDirection.startToEnd, |
| 351 | TextDirection.rtl || TextDirection.ltr => DismissDirection.endToStart, |
| 352 | }; |
| 353 | } |
| 354 | return extent > 0 ? DismissDirection.down : DismissDirection.up; |
| 355 | } |
| 356 | |
| 357 | DismissDirection get _dismissDirection => _extentToDirection(_dragExtent); |
| 358 | |
| 359 | double get _dismissThreshold => widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold; |
| 360 | |
| 361 | double get _overallDragAxisExtent { |
| 362 | final Size size = context.size!; |
| 363 | return _directionIsXAxis ? size.width : size.height; |
| 364 | } |
| 365 | |
| 366 | void _handleDragStart(DragStartDetails details) { |
| 367 | if (_confirming) { |
| 368 | return; |
| 369 | } |
| 370 | _dragUnderway = true; |
| 371 | if (_moveController.isAnimating) { |
| 372 | _dragExtent = _moveController.value * _overallDragAxisExtent * _dragExtent.sign; |
| 373 | _moveController.stop(); |
| 374 | } else { |
| 375 | _dragExtent = 0.0; |
| 376 | _moveController.value = 0.0; |
| 377 | } |
| 378 | setState(() { |
| 379 | _updateMoveAnimation(); |
| 380 | }); |
| 381 | } |
| 382 | |
| 383 | void _handleDragUpdate(DragUpdateDetails details) { |
| 384 | if (!_dragUnderway || _moveController.isAnimating) { |
| 385 | return; |
| 386 | } |
| 387 | |
| 388 | final double delta = details.primaryDelta!; |
| 389 | final double oldDragExtent = _dragExtent; |
| 390 | switch (widget.direction) { |
| 391 | case DismissDirection.horizontal: |
| 392 | case DismissDirection.vertical: |
| 393 | _dragExtent += delta; |
| 394 | |
| 395 | case DismissDirection.up: |
| 396 | if (_dragExtent + delta < 0) { |
| 397 | _dragExtent += delta; |
| 398 | } |
| 399 | |
| 400 | case DismissDirection.down: |
| 401 | if (_dragExtent + delta > 0) { |
| 402 | _dragExtent += delta; |
| 403 | } |
| 404 | |
| 405 | case DismissDirection.endToStart: |
| 406 | switch (Directionality.of(context)) { |
| 407 | case TextDirection.rtl: |
| 408 | if (_dragExtent + delta > 0) { |
| 409 | _dragExtent += delta; |
| 410 | } |
| 411 | case TextDirection.ltr: |
| 412 | if (_dragExtent + delta < 0) { |
| 413 | _dragExtent += delta; |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | case DismissDirection.startToEnd: |
| 418 | switch (Directionality.of(context)) { |
| 419 | case TextDirection.rtl: |
| 420 | if (_dragExtent + delta < 0) { |
| 421 | _dragExtent += delta; |
| 422 | } |
| 423 | case TextDirection.ltr: |
| 424 | if (_dragExtent + delta > 0) { |
| 425 | _dragExtent += delta; |
| 426 | } |
| 427 | } |
| 428 | |
| 429 | case DismissDirection.none: |
| 430 | _dragExtent = 0; |
| 431 | } |
| 432 | if (oldDragExtent.sign != _dragExtent.sign) { |
| 433 | setState(() { |
| 434 | _updateMoveAnimation(); |
| 435 | }); |
| 436 | } |
| 437 | if (!_moveController.isAnimating) { |
| 438 | _moveController.value = _dragExtent.abs() / _overallDragAxisExtent; |
| 439 | } |
| 440 | } |
| 441 | |
| 442 | void _handleDismissUpdateValueChanged() { |
| 443 | if (widget.onUpdate != null) { |
| 444 | final bool oldDismissThresholdReached = _dismissThresholdReached; |
| 445 | _dismissThresholdReached = _moveController.value > _dismissThreshold; |
| 446 | final DismissUpdateDetails details = DismissUpdateDetails( |
| 447 | direction: _dismissDirection, |
| 448 | reached: _dismissThresholdReached, |
| 449 | previousReached: oldDismissThresholdReached, |
| 450 | progress: _moveController.value, |
| 451 | ); |
| 452 | widget.onUpdate!(details); |
| 453 | } |
| 454 | } |
| 455 | |
| 456 | void _updateMoveAnimation() { |
| 457 | final double end = _dragExtent.sign; |
| 458 | _moveAnimation = _moveController.drive( |
| 459 | Tween<Offset>( |
| 460 | begin: Offset.zero, |
| 461 | end: _directionIsXAxis |
| 462 | ? Offset(end, widget.crossAxisEndOffset) |
| 463 | : Offset(widget.crossAxisEndOffset, end), |
| 464 | ), |
| 465 | ); |
| 466 | } |
| 467 | |
| 468 | _FlingGestureKind _describeFlingGesture(Velocity velocity) { |
| 469 | if (_dragExtent == 0.0) { |
| 470 | // If it was a fling, then it was a fling that was let loose at the exact |
| 471 | // middle of the range (i.e. when there's no displacement). In that case, |
| 472 | // we assume that the user meant to fling it back to the center, as |
| 473 | // opposed to having wanted to drag it out one way, then fling it past the |
| 474 | // center and into and out the other side. |
| 475 | return _FlingGestureKind.none; |
| 476 | } |
| 477 | final double vx = velocity.pixelsPerSecond.dx; |
| 478 | final double vy = velocity.pixelsPerSecond.dy; |
| 479 | DismissDirection flingDirection; |
| 480 | // Verify that the fling is in the generally right direction and fast enough. |
| 481 | if (_directionIsXAxis) { |
| 482 | if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta || vx.abs() < _kMinFlingVelocity) { |
| 483 | return _FlingGestureKind.none; |
| 484 | } |
| 485 | assert(vx != 0.0); |
| 486 | flingDirection = _extentToDirection(vx); |
| 487 | } else { |
| 488 | if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta || vy.abs() < _kMinFlingVelocity) { |
| 489 | return _FlingGestureKind.none; |
| 490 | } |
| 491 | assert(vy != 0.0); |
| 492 | flingDirection = _extentToDirection(vy); |
| 493 | } |
| 494 | if (flingDirection == _dismissDirection) { |
| 495 | return _FlingGestureKind.forward; |
| 496 | } |
| 497 | return _FlingGestureKind.reverse; |
| 498 | } |
| 499 | |
| 500 | void _handleDragEnd(DragEndDetails details) { |
| 501 | if (!_dragUnderway || _moveController.isAnimating) { |
| 502 | return; |
| 503 | } |
| 504 | _dragUnderway = false; |
| 505 | if (_moveController.isCompleted) { |
| 506 | _handleMoveCompleted(); |
| 507 | return; |
| 508 | } |
| 509 | final double flingVelocity = _directionIsXAxis |
| 510 | ? details.velocity.pixelsPerSecond.dx |
| 511 | : details.velocity.pixelsPerSecond.dy; |
| 512 | switch (_describeFlingGesture(details.velocity)) { |
| 513 | case _FlingGestureKind.forward: |
| 514 | assert(_dragExtent != 0.0); |
| 515 | assert(!_moveController.isDismissed); |
| 516 | if (_dismissThreshold >= 1.0) { |
| 517 | _moveController.reverse(); |
| 518 | break; |
| 519 | } |
| 520 | _dragExtent = flingVelocity.sign; |
| 521 | _moveController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale); |
| 522 | case _FlingGestureKind.reverse: |
| 523 | assert(_dragExtent != 0.0); |
| 524 | assert(!_moveController.isDismissed); |
| 525 | _dragExtent = flingVelocity.sign; |
| 526 | _moveController.fling(velocity: -flingVelocity.abs() * _kFlingVelocityScale); |
| 527 | case _FlingGestureKind.none: |
| 528 | if (!_moveController.isDismissed) { |
| 529 | // we already know it's not completed, we check that above |
| 530 | if (_moveController.value > _dismissThreshold) { |
| 531 | _moveController.forward(); |
| 532 | } else { |
| 533 | _moveController.reverse(); |
| 534 | } |
| 535 | } |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | Future<void> _handleDismissStatusChanged(AnimationStatus status) async { |
| 540 | if (status.isCompleted && !_dragUnderway) { |
| 541 | await _handleMoveCompleted(); |
| 542 | } |
| 543 | if (mounted) { |
| 544 | updateKeepAlive(); |
| 545 | } |
| 546 | } |
| 547 | |
| 548 | Future<void> _handleMoveCompleted() async { |
| 549 | if (_dismissThreshold >= 1.0) { |
| 550 | _moveController.reverse(); |
| 551 | return; |
| 552 | } |
| 553 | final bool result = await _confirmStartResizeAnimation(); |
| 554 | if (mounted) { |
| 555 | if (result) { |
| 556 | _startResizeAnimation(); |
| 557 | } else { |
| 558 | _moveController.reverse(); |
| 559 | } |
| 560 | } |
| 561 | } |
| 562 | |
| 563 | Future<bool> _confirmStartResizeAnimation() async { |
| 564 | if (widget.confirmDismiss != null) { |
| 565 | _confirming = true; |
| 566 | final DismissDirection direction = _dismissDirection; |
| 567 | try { |
| 568 | return await widget.confirmDismiss!(direction) ?? false; |
| 569 | } finally { |
| 570 | _confirming = false; |
| 571 | } |
| 572 | } |
| 573 | return true; |
| 574 | } |
| 575 | |
| 576 | void _startResizeAnimation() { |
| 577 | assert(_moveController.isCompleted); |
| 578 | assert(_resizeController == null); |
| 579 | assert(_sizePriorToCollapse == null); |
| 580 | if (widget.resizeDuration == null) { |
| 581 | if (widget.onDismissed != null) { |
| 582 | final DismissDirection direction = _dismissDirection; |
| 583 | widget.onDismissed!(direction); |
| 584 | } |
| 585 | } else { |
| 586 | _resizeController = AnimationController(duration: widget.resizeDuration, vsync: this) |
| 587 | ..addListener(_handleResizeProgressChanged) |
| 588 | ..addStatusListener((AnimationStatus status) => updateKeepAlive()); |
| 589 | _resizeController!.forward(); |
| 590 | setState(() { |
| 591 | _sizePriorToCollapse = context.size; |
| 592 | _resizeAnimation = _resizeController! |
| 593 | .drive(CurveTween(curve: _kResizeTimeCurve)) |
| 594 | .drive(Tween<double>(begin: 1.0, end: 0.0)); |
| 595 | }); |
| 596 | } |
| 597 | } |
| 598 | |
| 599 | void _handleResizeProgressChanged() { |
| 600 | if (_resizeController!.isCompleted) { |
| 601 | widget.onDismissed?.call(_dismissDirection); |
| 602 | } else { |
| 603 | widget.onResize?.call(); |
| 604 | } |
| 605 | } |
| 606 | |
| 607 | @override |
| 608 | Widget build(BuildContext context) { |
| 609 | super.build(context); // See AutomaticKeepAliveClientMixin. |
| 610 | |
| 611 | assert(!_directionIsXAxis || debugCheckHasDirectionality(context)); |
| 612 | |
| 613 | Widget? background = widget.background; |
| 614 | if (widget.secondaryBackground != null) { |
| 615 | final DismissDirection direction = _dismissDirection; |
| 616 | if (direction == DismissDirection.endToStart || direction == DismissDirection.up) { |
| 617 | background = widget.secondaryBackground; |
| 618 | } |
| 619 | } |
| 620 | |
| 621 | if (_resizeAnimation != null) { |
| 622 | // we've been dragged aside, and are now resizing. |
| 623 | assert(() { |
| 624 | if (_resizeAnimation!.status != AnimationStatus.forward) { |
| 625 | assert(_resizeAnimation!.isCompleted); |
| 626 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
| 627 | ErrorSummary('A dismissed Dismissible widget is still part of the tree.' ), |
| 628 | ErrorHint( |
| 629 | 'Make sure to implement the onDismissed handler and to immediately remove the Dismissible ' |
| 630 | 'widget from the application once that handler has fired.' , |
| 631 | ), |
| 632 | ]); |
| 633 | } |
| 634 | return true; |
| 635 | }()); |
| 636 | |
| 637 | return SizeTransition( |
| 638 | sizeFactor: _resizeAnimation!, |
| 639 | axis: _directionIsXAxis ? Axis.vertical : Axis.horizontal, |
| 640 | child: SizedBox( |
| 641 | width: _sizePriorToCollapse!.width, |
| 642 | height: _sizePriorToCollapse!.height, |
| 643 | child: background, |
| 644 | ), |
| 645 | ); |
| 646 | } |
| 647 | |
| 648 | Widget content = SlideTransition( |
| 649 | position: _moveAnimation, |
| 650 | child: KeyedSubtree(key: _contentKey, child: widget.child), |
| 651 | ); |
| 652 | |
| 653 | if (background != null) { |
| 654 | content = Stack( |
| 655 | children: <Widget>[ |
| 656 | if (!_moveAnimation.isDismissed) |
| 657 | Positioned.fill( |
| 658 | child: ClipRect( |
| 659 | clipper: _DismissibleClipper( |
| 660 | axis: _directionIsXAxis ? Axis.horizontal : Axis.vertical, |
| 661 | moveAnimation: _moveAnimation, |
| 662 | ), |
| 663 | child: background, |
| 664 | ), |
| 665 | ), |
| 666 | content, |
| 667 | ], |
| 668 | ); |
| 669 | } |
| 670 | |
| 671 | // If the DismissDirection is none, we do not add drag gestures because the content |
| 672 | // cannot be dragged. |
| 673 | if (widget.direction == DismissDirection.none) { |
| 674 | return content; |
| 675 | } |
| 676 | |
| 677 | // We are not resizing but we may be being dragging in widget.direction. |
| 678 | return GestureDetector( |
| 679 | onHorizontalDragStart: _directionIsXAxis ? _handleDragStart : null, |
| 680 | onHorizontalDragUpdate: _directionIsXAxis ? _handleDragUpdate : null, |
| 681 | onHorizontalDragEnd: _directionIsXAxis ? _handleDragEnd : null, |
| 682 | onVerticalDragStart: _directionIsXAxis ? null : _handleDragStart, |
| 683 | onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate, |
| 684 | onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd, |
| 685 | behavior: widget.behavior, |
| 686 | dragStartBehavior: widget.dragStartBehavior, |
| 687 | child: content, |
| 688 | ); |
| 689 | } |
| 690 | } |
| 691 | |