dismissible.dart 23.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'package:flutter/gestures.dart';

7
import 'automatic_keep_alive.dart';
8
import 'basic.dart';
Ian Hickson's avatar
Ian Hickson committed
9
import 'debug.dart';
10 11
import 'framework.dart';
import 'gesture_detector.dart';
12 13
import 'ticker_provider.dart';
import 'transitions.dart';
14

15
const Curve _kResizeTimeCurve = Interval(0.4, 1.0, curve: Curves.ease);
16 17
const double _kMinFlingVelocity = 700.0;
const double _kMinFlingVelocityDelta = 400.0;
18
const double _kFlingVelocityScale = 1.0 / 300.0;
Hans Muller's avatar
Hans Muller committed
19
const double _kDismissThreshold = 0.4;
20

21
/// Signature used by [Dismissible] to indicate that it has been dismissed in
22 23
/// the given `direction`.
///
24
/// Used by [Dismissible.onDismissed].
25
typedef DismissDirectionCallback = void Function(DismissDirection direction);
26

27 28 29 30
/// Signature used by [Dismissible] to give the application an opportunity to
/// confirm or veto a dismiss gesture.
///
/// Used by [Dismissible.confirmDismiss].
31
typedef ConfirmDismissCallback = Future<bool?> Function(DismissDirection direction);
32

33 34 35 36 37
/// Signature used by [Dismissible] to indicate that the dismissible has been dragged.
///
/// Used by [Dismissible.onUpdate].
typedef DismissUpdateCallback = void Function(DismissUpdateDetails details);

38
/// The direction in which a [Dismissible] can be dismissed.
39
enum DismissDirection {
40
  /// The [Dismissible] can be dismissed by dragging either up or down.
41
  vertical,
Adam Barth's avatar
Adam Barth committed
42

43
  /// The [Dismissible] can be dismissed by dragging either left or right.
44
  horizontal,
Adam Barth's avatar
Adam Barth committed
45

46
  /// The [Dismissible] can be dismissed by dragging in the reverse of the
47 48
  /// reading direction (e.g., from right to left in left-to-right languages).
  endToStart,
Adam Barth's avatar
Adam Barth committed
49

50
  /// The [Dismissible] can be dismissed by dragging in the reading direction
51 52
  /// (e.g., from left to right in left-to-right languages).
  startToEnd,
Adam Barth's avatar
Adam Barth committed
53

54
  /// The [Dismissible] can be dismissed by dragging up only.
55
  up,
Adam Barth's avatar
Adam Barth committed
56

57
  /// The [Dismissible] can be dismissed by dragging down only.
58 59 60 61
  down,

  /// The [Dismissible] cannot be dismissed by dragging.
  none
62 63
}

64
/// A widget that can be dismissed by dragging in the indicated [direction].
Adam Barth's avatar
Adam Barth committed
65
///
66
/// Dragging or flinging this widget in the [DismissDirection] causes the child
67
/// to slide out of view. Following the slide animation, if [resizeDuration] is
68
/// non-null, the Dismissible widget animates its height (or width, whichever is
69
/// perpendicular to the dismiss direction) to zero over the [resizeDuration].
70
///
71 72
/// {@youtube 560 315 https://www.youtube.com/watch?v=iEMgjrfuc58}
///
73
/// {@tool dartpad}
74
/// This sample shows how you can use the [Dismissible] widget to
75 76
/// remove list items using swipe gestures. Swipe any of the list
/// tiles to the left or right to dismiss them from the [ListView].
77
///
78
/// ** See code in examples/api/lib/widgets/dismissible/dismissible.0.dart **
79 80
/// {@end-tool}
///
81
/// Backgrounds can be used to implement the "leave-behind" idiom. If a background
82
/// is specified it is stacked behind the Dismissible's child and is exposed when
83 84
/// the child moves.
///
85
/// The widget calls the [onDismissed] callback either after its size has
86
/// collapsed to zero (if [resizeDuration] is non-null) or immediately after
87
/// the slide animation (if [resizeDuration] is null). If the Dismissible is a
88 89
/// list item, it must have a key that distinguishes it from the other items and
/// its [onDismissed] callback must remove the item from the list.
90
class Dismissible extends StatefulWidget {
91 92
  /// Creates a widget that can be dismissed.
  ///
93 94 95 96 97 98
  /// The [key] argument is required because [Dismissible]s are commonly used in
  /// lists and removed from the list when dismissed. Without keys, the default
  /// behavior is to sync widgets based on their index in the list, which means
  /// the item after the dismissed item would be synced with the state of the
  /// dismissed item. Using keys causes the widgets to sync according to their
  /// keys and avoids this pitfall.
99
  const Dismissible({
100 101
    required Key key,
    required this.child,
102 103
    this.background,
    this.secondaryBackground,
104
    this.confirmDismiss,
105
    this.onResize,
106
    this.onUpdate,
107
    this.onDismissed,
108 109 110 111 112
    this.direction = DismissDirection.horizontal,
    this.resizeDuration = const Duration(milliseconds: 300),
    this.dismissThresholds = const <DismissDirection, double>{},
    this.movementDuration = const Duration(milliseconds: 200),
    this.crossAxisEndOffset = 0.0,
113
    this.dragStartBehavior = DragStartBehavior.start,
114
    this.behavior = HitTestBehavior.opaque,
115
  }) : assert(secondaryBackground == null || background != null),
116
       super(key: key);
117

118
  /// The widget below this widget in the tree.
119
  ///
120
  /// {@macro flutter.widgets.ProxyWidget.child}
121
  final Widget child;
Adam Barth's avatar
Adam Barth committed
122

123 124 125
  /// A widget that is stacked behind the child. If secondaryBackground is also
  /// specified then this widget only appears when the child has been dragged
  /// down or to the right.
126
  final Widget? background;
127 128 129 130

  /// A widget that is stacked behind the child and is exposed when the child
  /// has been dragged up or to the left. It may only be specified when background
  /// has also been specified.
131
  final Widget? secondaryBackground;
132

133 134
  /// Gives the app an opportunity to confirm or veto a pending dismissal.
  ///
135 136
  /// The widget cannot be dragged again until the returned future resolves.
  ///
137 138 139
  /// If the returned Future<bool> completes true, then this widget will be
  /// dismissed, otherwise it will be moved back to its original location.
  ///
140
  /// If the returned Future<bool?> completes to false or null the [onResize]
141
  /// and [onDismissed] callbacks will not run.
142
  final ConfirmDismissCallback? confirmDismiss;
143

144
  /// Called when the widget changes size (i.e., when contracting before being dismissed).
145
  final VoidCallback? onResize;
Adam Barth's avatar
Adam Barth committed
146

147
  /// Called when the widget has been dismissed, after finishing resizing.
148
  final DismissDirectionCallback? onDismissed;
Adam Barth's avatar
Adam Barth committed
149 150

  /// The direction in which the widget can be dismissed.
151
  final DismissDirection direction;
152

153 154 155
  /// The amount of time the widget will spend contracting before [onDismissed] is called.
  ///
  /// If null, the widget will not contract and [onDismissed] will be called
156
  /// immediately after the widget is dismissed.
157
  final Duration? resizeDuration;
158

Ian Hickson's avatar
Ian Hickson committed
159 160
  /// The offset threshold the item has to be dragged in order to be considered
  /// dismissed.
161
  ///
Ian Hickson's avatar
Ian Hickson committed
162 163 164 165 166 167 168 169
  /// Represented as a fraction, e.g. if it is 0.4 (the default), then the item
  /// has to be dragged at least 40% towards one direction to be considered
  /// dismissed. Clients can define different thresholds for each dismiss
  /// direction.
  ///
  /// Flinging is treated as being equivalent to dragging almost to 1.0, so
  /// flinging can dismiss an item past any threshold less than 1.0.
  ///
170 171 172 173 174 175 176 177
  /// Setting a threshold of 1.0 (or greater) prevents a drag in the given
  /// [DismissDirection] even if it would be allowed by the [direction]
  /// property.
  ///
  /// See also:
  ///
  ///  * [direction], which controls the directions in which the items can
  ///    be dismissed.
178 179
  final Map<DismissDirection, double> dismissThresholds;

180 181 182 183 184 185 186 187 188
  /// Defines the duration for card to dismiss or to come back to original position if not dismissed.
  final Duration movementDuration;

  /// Defines the end offset across the main axis after the card is dismissed.
  ///
  /// If non-zero value is given then widget moves in cross direction depending on whether
  /// it is positive or negative.
  final double crossAxisEndOffset;

189 190 191
  /// Determines the way that drag start behavior is handled.
  ///
  /// If set to [DragStartBehavior.start], the drag gesture used to dismiss a
192 193 194
  /// dismissible will begin at the position where the drag gesture won the arena.
  /// If set to [DragStartBehavior.down] it will begin at the position where
  /// a down event is first detected.
195 196 197 198 199
  ///
  /// In general, setting this to [DragStartBehavior.start] will make drag
  /// animation smoother and setting it to [DragStartBehavior.down] will make
  /// drag behavior feel slightly more reactive.
  ///
200
  /// By default, the drag start behavior is [DragStartBehavior.start].
201 202 203 204 205 206
  ///
  /// See also:
  ///
  ///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
  final DragStartBehavior dragStartBehavior;

207 208 209 210 211
  /// How to behave during hit tests.
  ///
  /// This defaults to [HitTestBehavior.opaque].
  final HitTestBehavior behavior;

212 213 214 215 216 217 218 219
  /// Called when the dismissible widget has been dragged.
  ///
  /// If [onUpdate] is not null, then it will be invoked for every pointer event
  /// to dispatch the latest state of the drag. For example, this callback
  /// can be used to for example change the color of the background widget
  /// depending on whether the dismiss threshold is currently reached.
  final DismissUpdateCallback? onUpdate;

220
  @override
221
  State<Dismissible> createState() => _DismissibleState();
222
}
223

224 225 226 227 228 229 230 231 232 233
/// Details for [DismissUpdateCallback].
///
/// See also:
///
///   * [Dismissible.onUpdate], which receives this information.
class DismissUpdateDetails {
  /// Create a new instance of [DismissUpdateDetails].
  DismissUpdateDetails({
    this.direction = DismissDirection.horizontal,
    this.reached = false,
234 235
    this.previousReached = false,
    this.progress = 0.0,
236 237 238 239 240 241 242 243 244 245 246 247 248
  });

  /// The direction that the dismissible is being dragged.
  final DismissDirection direction;

  /// Whether the dismiss threshold is currently reached.
  final bool reached;

  /// Whether the dismiss threshold was reached the last time this callback was invoked.
  ///
  /// This can be used in conjunction with [DismissUpdateDetails.reached] to catch the moment
  /// that the [Dismissible] is dragged across the threshold.
  final bool previousReached;
249 250 251 252 253 254 255 256 257

  /// The offset ratio of the dismissible in its parent container.
  ///
  /// A value of 0.0 represents the normal position and 1.0 means the child is
  /// completely outside its parent.
  ///
  /// This can be used to synchronize other elements to what the dismissible is doing on screen,
  /// e.g. using this value to set the opacity thereby fading dismissible as it's dragged offscreen.
  final double progress;
258 259
}

260 261
class _DismissibleClipper extends CustomClipper<Rect> {
  _DismissibleClipper({
262 263
    required this.axis,
    required this.moveAnimation,
264
  }) : super(reclip: moveAnimation);
265 266

  final Axis axis;
267
  final Animation<Offset> moveAnimation;
268 269 270 271 272

  @override
  Rect getClip(Size size) {
    switch (axis) {
      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 288 289 290
    }
  }

  @override
  Rect getApproximateClipRect(Size size) => getClip(size);

  @override
291
  bool shouldReclip(_DismissibleClipper oldClipper) {
292 293 294 295 296
    return oldClipper.axis != axis
        || oldClipper.moveAnimation.value != moveAnimation.value;
  }
}

Ian Hickson's avatar
Ian Hickson committed
297 298
enum _FlingGestureKind { none, forward, reverse }

299
class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
300
  @override
301
  void initState() {
302
    super.initState();
303
    _moveController = AnimationController(duration: widget.movementDuration, vsync: this)
304 305
      ..addStatusListener(_handleDismissStatusChanged)
      ..addListener(_handleDismissUpdateValueChanged);
306
    _updateMoveAnimation();
307 308
  }

309 310
  AnimationController? _moveController;
  late Animation<Offset> _moveAnimation;
311

312 313
  AnimationController? _resizeController;
  Animation<double>? _resizeAnimation;
314 315

  double _dragExtent = 0.0;
316
  bool _confirming = false;
317
  bool _dragUnderway = false;
318
  Size? _sizePriorToCollapse;
319
  bool _dismissThresholdReached = false;
320

321 322
  final GlobalKey _contentKey = GlobalKey();

323
  @override
324
  bool get wantKeepAlive => (_moveController?.isAnimating ?? false) || (_resizeController?.isAnimating ?? false);
325

326
  @override
327
  void dispose() {
328
    _moveController!.dispose();
329
    _resizeController?.dispose();
330 331 332
    super.dispose();
  }

333
  bool get _directionIsXAxis {
334 335 336
    return widget.direction == DismissDirection.horizontal
        || widget.direction == DismissDirection.endToStart
        || widget.direction == DismissDirection.startToEnd;
337 338
  }

339
  DismissDirection _extentToDirection(double extent) {
340
    if (extent == 0.0) {
341
      return DismissDirection.none;
342
    }
Ian Hickson's avatar
Ian Hickson committed
343
    if (_directionIsXAxis) {
344 345 346 347 348
      return switch (Directionality.of(context)) {
        TextDirection.rtl when extent < 0 => DismissDirection.startToEnd,
        TextDirection.ltr when extent > 0 => DismissDirection.startToEnd,
        TextDirection.rtl || TextDirection.ltr => DismissDirection.endToStart,
      };
Ian Hickson's avatar
Ian Hickson committed
349 350
    }
    return extent > 0 ? DismissDirection.down : DismissDirection.up;
351 352
  }

353
  DismissDirection get _dismissDirection => _extentToDirection(_dragExtent);
354

355
  bool get _isActive {
356
    return _dragUnderway || _moveController!.isAnimating;
357 358
  }

359
  double get _overallDragAxisExtent {
360
    final Size size = context.size!;
361
    return _directionIsXAxis ? size.width : size.height;
362 363
  }

364
  void _handleDragStart(DragStartDetails details) {
365
    if (_confirming) {
366
      return;
367
    }
368
    _dragUnderway = true;
369 370 371
    if (_moveController!.isAnimating) {
      _dragExtent = _moveController!.value * _overallDragAxisExtent * _dragExtent.sign;
      _moveController!.stop();
372 373
    } else {
      _dragExtent = 0.0;
374
      _moveController!.value = 0.0;
375
    }
376
    setState(() {
377
      _updateMoveAnimation();
378
    });
379 380
  }

381
  void _handleDragUpdate(DragUpdateDetails details) {
382
    if (!_isActive || _moveController!.isAnimating) {
383
      return;
384
    }
385

386
    final double delta = details.primaryDelta!;
387
    final double oldDragExtent = _dragExtent;
388
    switch (widget.direction) {
389 390
      case DismissDirection.horizontal:
      case DismissDirection.vertical:
391
        _dragExtent += delta;
392 393

      case DismissDirection.up:
394
        if (_dragExtent + delta < 0) {
395
          _dragExtent += delta;
396
        }
397 398

      case DismissDirection.down:
399
        if (_dragExtent + delta > 0) {
400
          _dragExtent += delta;
401
        }
Ian Hickson's avatar
Ian Hickson committed
402 403

      case DismissDirection.endToStart:
404
        switch (Directionality.of(context)) {
Ian Hickson's avatar
Ian Hickson committed
405
          case TextDirection.rtl:
406
            if (_dragExtent + delta > 0) {
Ian Hickson's avatar
Ian Hickson committed
407
              _dragExtent += delta;
408
            }
Ian Hickson's avatar
Ian Hickson committed
409
          case TextDirection.ltr:
410
            if (_dragExtent + delta < 0) {
Ian Hickson's avatar
Ian Hickson committed
411
              _dragExtent += delta;
412
            }
Ian Hickson's avatar
Ian Hickson committed
413 414 415
        }

      case DismissDirection.startToEnd:
416
        switch (Directionality.of(context)) {
Ian Hickson's avatar
Ian Hickson committed
417
          case TextDirection.rtl:
418
            if (_dragExtent + delta < 0) {
Ian Hickson's avatar
Ian Hickson committed
419
              _dragExtent += delta;
420
            }
Ian Hickson's avatar
Ian Hickson committed
421
          case TextDirection.ltr:
422
            if (_dragExtent + delta > 0) {
Ian Hickson's avatar
Ian Hickson committed
423
              _dragExtent += delta;
424
            }
Ian Hickson's avatar
Ian Hickson committed
425
        }
426 427 428

      case DismissDirection.none:
        _dragExtent = 0;
429
    }
430 431
    if (oldDragExtent.sign != _dragExtent.sign) {
      setState(() {
432
        _updateMoveAnimation();
433 434
      });
    }
435 436
    if (!_moveController!.isAnimating) {
      _moveController!.value = _dragExtent.abs() / _overallDragAxisExtent;
437 438 439
    }
  }

440
  void _handleDismissUpdateValueChanged() {
441
    if (widget.onUpdate != null) {
442 443 444 445 446 447
      final bool oldDismissThresholdReached = _dismissThresholdReached;
      _dismissThresholdReached = _moveController!.value > (widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold);
      final DismissUpdateDetails details = DismissUpdateDetails(
          direction: _dismissDirection,
          reached: _dismissThresholdReached,
          previousReached: oldDismissThresholdReached,
448
          progress: _moveController!.value,
449 450 451 452 453
      );
      widget.onUpdate!(details);
    }
  }

454
  void _updateMoveAnimation() {
455
    final double end = _dragExtent.sign;
456
    _moveAnimation = _moveController!.drive(
457 458 459
      Tween<Offset>(
        begin: Offset.zero,
        end: _directionIsXAxis
460 461
            ? Offset(end, widget.crossAxisEndOffset)
            : Offset(widget.crossAxisEndOffset, end),
462 463
      ),
    );
464 465
  }

Ian Hickson's avatar
Ian Hickson committed
466 467 468 469 470 471 472 473 474
  _FlingGestureKind _describeFlingGesture(Velocity velocity) {
    if (_dragExtent == 0.0) {
      // If it was a fling, then it was a fling that was let loose at the exact
      // middle of the range (i.e. when there's no displacement). In that case,
      // we assume that the user meant to fling it back to the center, as
      // opposed to having wanted to drag it out one way, then fling it past the
      // center and into and out the other side.
      return _FlingGestureKind.none;
    }
475 476
    final double vx = velocity.pixelsPerSecond.dx;
    final double vy = velocity.pixelsPerSecond.dy;
477
    DismissDirection flingDirection;
Ian Hickson's avatar
Ian Hickson committed
478
    // Verify that the fling is in the generally right direction and fast enough.
479
    if (_directionIsXAxis) {
480
      if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta || vx.abs() < _kMinFlingVelocity) {
Ian Hickson's avatar
Ian Hickson committed
481
        return _FlingGestureKind.none;
482
      }
Ian Hickson's avatar
Ian Hickson committed
483 484
      assert(vx != 0.0);
      flingDirection = _extentToDirection(vx);
485
    } else {
486
      if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta || vy.abs() < _kMinFlingVelocity) {
Ian Hickson's avatar
Ian Hickson committed
487
        return _FlingGestureKind.none;
488
      }
Ian Hickson's avatar
Ian Hickson committed
489 490
      assert(vy != 0.0);
      flingDirection = _extentToDirection(vy);
491
    }
492
    if (flingDirection == _dismissDirection) {
Ian Hickson's avatar
Ian Hickson committed
493
      return _FlingGestureKind.forward;
494
    }
Ian Hickson's avatar
Ian Hickson committed
495
    return _FlingGestureKind.reverse;
496 497
  }

498
  void _handleDragEnd(DragEndDetails details) {
499
    if (!_isActive || _moveController!.isAnimating) {
500
      return;
501
    }
502
    _dragUnderway = false;
503 504
    if (_moveController!.isCompleted) {
      _handleMoveCompleted();
Ian Hickson's avatar
Ian Hickson committed
505 506 507 508 509 510
      return;
    }
    final double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy;
    switch (_describeFlingGesture(details.velocity)) {
      case _FlingGestureKind.forward:
        assert(_dragExtent != 0.0);
511
        assert(!_moveController!.isDismissed);
Ian Hickson's avatar
Ian Hickson committed
512
        if ((widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold) >= 1.0) {
513
          _moveController!.reverse();
Ian Hickson's avatar
Ian Hickson committed
514 515 516
          break;
        }
        _dragExtent = flingVelocity.sign;
517
        _moveController!.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
Ian Hickson's avatar
Ian Hickson committed
518 519
      case _FlingGestureKind.reverse:
        assert(_dragExtent != 0.0);
520
        assert(!_moveController!.isDismissed);
Ian Hickson's avatar
Ian Hickson committed
521
        _dragExtent = flingVelocity.sign;
522
        _moveController!.fling(velocity: -flingVelocity.abs() * _kFlingVelocityScale);
Ian Hickson's avatar
Ian Hickson committed
523
      case _FlingGestureKind.none:
524 525 526
        if (!_moveController!.isDismissed) { // we already know it's not completed, we check that above
          if (_moveController!.value > (widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold)) {
            _moveController!.forward();
Ian Hickson's avatar
Ian Hickson committed
527
          } else {
528
            _moveController!.reverse();
Ian Hickson's avatar
Ian Hickson committed
529 530
          }
        }
531
    }
532 533
  }

534 535
  Future<void> _handleDismissStatusChanged(AnimationStatus status) async {
    if (status == AnimationStatus.completed && !_dragUnderway) {
536 537 538 539 540 541 542 543 544 545 546 547 548 549
      await _handleMoveCompleted();
    }
    if (mounted) {
      updateKeepAlive();
    }
  }

  Future<void> _handleMoveCompleted() async {
    if ((widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold) >= 1.0) {
      _moveController!.reverse();
      return;
    }
    final bool result = await _confirmStartResizeAnimation();
    if (mounted) {
550
      if (result) {
551
        _startResizeAnimation();
552
      } else {
553
        _moveController!.reverse();
554
      }
555
    }
556 557
  }

558
  Future<bool> _confirmStartResizeAnimation() async {
559
    if (widget.confirmDismiss != null) {
560
      _confirming = true;
561
      final DismissDirection direction = _dismissDirection;
562 563 564 565 566
      try {
        return await widget.confirmDismiss!(direction) ?? false;
      } finally {
        _confirming = false;
      }
567 568 569 570
    }
    return true;
  }

571
  void _startResizeAnimation() {
572
    assert(_moveController!.isCompleted);
573
    assert(_resizeController == null);
574
    assert(_sizePriorToCollapse == null);
575
    if (widget.resizeDuration == null) {
Ian Hickson's avatar
Ian Hickson committed
576
      if (widget.onDismissed != null) {
577
        final DismissDirection direction = _dismissDirection;
578
        widget.onDismissed!(direction);
Ian Hickson's avatar
Ian Hickson committed
579
      }
580
    } else {
581
      _resizeController = AnimationController(duration: widget.resizeDuration, vsync: this)
582 583
        ..addListener(_handleResizeProgressChanged)
        ..addStatusListener((AnimationStatus status) => updateKeepAlive());
584
      _resizeController!.forward();
585
      setState(() {
586
        _sizePriorToCollapse = context.size;
587
        _resizeAnimation = _resizeController!.drive(
588
          CurveTween(
589
            curve: _kResizeTimeCurve,
590 591 592 593
          ),
        ).drive(
          Tween<double>(
            begin: 1.0,
594
            end: 0.0,
595 596
          ),
        );
597 598
      });
    }
599
  }
600

601
  void _handleResizeProgressChanged() {
602
    if (_resizeController!.isCompleted) {
603
      widget.onDismissed?.call(_dismissDirection);
604
    } else {
605
      widget.onResize?.call();
606 607 608
    }
  }

609
  @override
610
  Widget build(BuildContext context) {
611
    super.build(context); // See AutomaticKeepAliveClientMixin.
Ian Hickson's avatar
Ian Hickson committed
612 613 614

    assert(!_directionIsXAxis || debugCheckHasDirectionality(context));

615
    Widget? background = widget.background;
616
    if (widget.secondaryBackground != null) {
617
      final DismissDirection direction = _dismissDirection;
618
      if (direction == DismissDirection.endToStart || direction == DismissDirection.up) {
619
        background = widget.secondaryBackground;
620
      }
621 622
    }

623 624 625
    if (_resizeAnimation != null) {
      // we've been dragged aside, and are now resizing.
      assert(() {
626 627
        if (_resizeAnimation!.status != AnimationStatus.forward) {
          assert(_resizeAnimation!.status == AnimationStatus.completed);
628 629 630 631
          throw FlutterError.fromParts(<DiagnosticsNode>[
            ErrorSummary('A dismissed Dismissible widget is still part of the tree.'),
            ErrorHint(
              'Make sure to implement the onDismissed handler and to immediately remove the Dismissible '
632 633
              'widget from the application once that handler has fired.',
            ),
634
          ]);
635 636
        }
        return true;
637
      }());
638

639
      return SizeTransition(
640
        sizeFactor: _resizeAnimation!,
641
        axis: _directionIsXAxis ? Axis.vertical : Axis.horizontal,
642
        child: SizedBox(
643 644
          width: _sizePriorToCollapse!.width,
          height: _sizePriorToCollapse!.height,
645 646
          child: background,
        ),
647
      );
Adam Barth's avatar
Adam Barth committed
648
    }
649

650
    Widget content = SlideTransition(
651
      position: _moveAnimation,
652
      child: KeyedSubtree(key: _contentKey, child: widget.child),
653
    );
654

655
    if (background != null) {
656 657 658 659 660 661 662 663 664
      content = Stack(children: <Widget>[
        if (!_moveAnimation.isDismissed)
          Positioned.fill(
            child: ClipRect(
              clipper: _DismissibleClipper(
                axis: _directionIsXAxis ? Axis.horizontal : Axis.vertical,
                moveAnimation: _moveAnimation,
              ),
              child: background,
665
            ),
666
          ),
667 668
        content,
      ]);
669
    }
670 671 672

    // If the DismissDirection is none, we do not add drag gestures because the content
    // 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 680 681 682 683 684
      onHorizontalDragStart: _directionIsXAxis ? _handleDragStart : null,
      onHorizontalDragUpdate: _directionIsXAxis ? _handleDragUpdate : null,
      onHorizontalDragEnd: _directionIsXAxis ? _handleDragEnd : null,
      onVerticalDragStart: _directionIsXAxis ? null : _handleDragStart,
      onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate,
      onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd,
685
      behavior: widget.behavior,
686
      dragStartBehavior: widget.dragStartBehavior,
687
      child: content,
688 689 690
    );
  }
}