dismissible.dart 21.5 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
/// The direction in which a [Dismissible] can be dismissed.
34
enum DismissDirection {
35
  /// The [Dismissible] can be dismissed by dragging either up or down.
36
  vertical,
Adam Barth's avatar
Adam Barth committed
37

38
  /// The [Dismissible] can be dismissed by dragging either left or right.
39
  horizontal,
Adam Barth's avatar
Adam Barth committed
40

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

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

49
  /// The [Dismissible] can be dismissed by dragging up only.
50
  up,
Adam Barth's avatar
Adam Barth committed
51

52
  /// The [Dismissible] can be dismissed by dragging down only.
53 54 55 56
  down,

  /// The [Dismissible] cannot be dismissed by dragging.
  none
57 58
}

59
/// A widget that can be dismissed by dragging in the indicated [direction].
Adam Barth's avatar
Adam Barth committed
60
///
61
/// Dragging or flinging this widget in the [DismissDirection] causes the child
62
/// to slide out of view. Following the slide animation, if [resizeDuration] is
63
/// non-null, the Dismissible widget animates its height (or width, whichever is
64
/// perpendicular to the dismiss direction) to zero over the [resizeDuration].
65
///
66 67
/// {@youtube 560 315 https://www.youtube.com/watch?v=iEMgjrfuc58}
///
68
/// {@tool dartpad --template=stateful_widget_scaffold}
69 70
///
/// This sample shows how you can use the [Dismissible] widget to
71 72
/// remove list items using swipe gestures. Swipe any of the list
/// tiles to the left or right to dismiss them from the [ListView].
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
///
/// ```dart
/// List<int> items = List<int>.generate(100, (index) => index);
///
/// Widget build(BuildContext context) {
///   return ListView.builder(
///     itemCount: items.length,
///     padding: const EdgeInsets.symmetric(vertical: 16),
///     itemBuilder: (BuildContext context, int index) {
///       return Dismissible(
///         child: ListTile(
///           title: Text(
///             'Item ${items[index]}',
///           ),
///         ),
///         background: Container(
///           color: Colors.green,
///         ),
///         key: ValueKey(items[index]),
///         onDismissed: (DismissDirection direction) {
///           setState(() {
///             items.remove(index);
///           });
///         },
///       );
///     },
///   );
/// }
/// ```
/// {@end-tool}
///
104
/// Backgrounds can be used to implement the "leave-behind" idiom. If a background
105
/// is specified it is stacked behind the Dismissible's child and is exposed when
106 107
/// the child moves.
///
108
/// The widget calls the [onDismissed] callback either after its size has
109
/// collapsed to zero (if [resizeDuration] is non-null) or immediately after
110
/// the slide animation (if [resizeDuration] is null). If the Dismissible is a
111 112
/// 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.
113
class Dismissible extends StatefulWidget {
114 115
  /// Creates a widget that can be dismissed.
  ///
116
  /// The [key] argument must not be null because [Dismissible]s are commonly
117 118 119 120 121
  /// 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.
122
  const Dismissible({
123 124
    required Key key,
    required this.child,
125 126
    this.background,
    this.secondaryBackground,
127
    this.confirmDismiss,
128
    this.onResize,
129
    this.onDismissed,
130 131 132 133 134
    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,
135
    this.dragStartBehavior = DragStartBehavior.start,
136
    this.behavior = HitTestBehavior.opaque,
137
  }) : assert(key != null),
138
       assert(secondaryBackground == null || background != null),
139
       assert(dragStartBehavior != null),
140
       super(key: key);
141

142
  /// The widget below this widget in the tree.
143
  ///
144
  /// {@macro flutter.widgets.ProxyWidget.child}
145
  final Widget child;
Adam Barth's avatar
Adam Barth committed
146

147 148 149
  /// 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.
150
  final Widget? background;
151 152 153 154

  /// 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.
155
  final Widget? secondaryBackground;
156

157 158 159 160 161
  /// Gives the app an opportunity to confirm or veto a pending dismissal.
  ///
  /// If the returned Future<bool> completes true, then this widget will be
  /// dismissed, otherwise it will be moved back to its original location.
  ///
162
  /// If the returned Future<bool?> completes to false or null the [onResize]
163
  /// and [onDismissed] callbacks will not run.
164
  final ConfirmDismissCallback? confirmDismiss;
165

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

169
  /// Called when the widget has been dismissed, after finishing resizing.
170
  final DismissDirectionCallback? onDismissed;
Adam Barth's avatar
Adam Barth committed
171 172

  /// The direction in which the widget can be dismissed.
173
  final DismissDirection direction;
174

175 176 177
  /// 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
178
  /// immediately after the widget is dismissed.
179
  final Duration? resizeDuration;
180

Ian Hickson's avatar
Ian Hickson committed
181 182
  /// The offset threshold the item has to be dragged in order to be considered
  /// dismissed.
183
  ///
Ian Hickson's avatar
Ian Hickson committed
184 185 186 187 188 189 190 191
  /// 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.
  ///
192 193 194 195 196 197 198 199
  /// 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.
200 201
  final Map<DismissDirection, double> dismissThresholds;

202 203 204 205 206 207 208 209 210
  /// 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;

211 212 213 214 215 216 217 218 219 220
  /// Determines the way that drag start behavior is handled.
  ///
  /// If set to [DragStartBehavior.start], the drag gesture used to dismiss a
  /// dismissible will begin upon the detection of a drag gesture. If set to
  /// [DragStartBehavior.down] it will begin when a down event is first detected.
  ///
  /// 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.
  ///
221
  /// By default, the drag start behavior is [DragStartBehavior.start].
222 223 224 225 226 227
  ///
  /// See also:
  ///
  ///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
  final DragStartBehavior dragStartBehavior;

228 229 230 231 232
  /// How to behave during hit tests.
  ///
  /// This defaults to [HitTestBehavior.opaque].
  final HitTestBehavior behavior;

233
  @override
234
  _DismissibleState createState() => _DismissibleState();
235
}
236

237 238
class _DismissibleClipper extends CustomClipper<Rect> {
  _DismissibleClipper({
239 240
    required this.axis,
    required this.moveAnimation,
241 242 243
  }) : assert(axis != null),
       assert(moveAnimation != null),
       super(reclip: moveAnimation);
244 245

  final Axis axis;
246
  final Animation<Offset> moveAnimation;
247 248 249 250 251 252

  @override
  Rect getClip(Size size) {
    assert(axis != null);
    switch (axis) {
      case Axis.horizontal:
253
        final double offset = moveAnimation.value.dx * size.width;
254
        if (offset < 0)
255 256
          return Rect.fromLTRB(size.width + offset, 0.0, size.width, size.height);
        return Rect.fromLTRB(0.0, 0.0, offset, size.height);
257
      case Axis.vertical:
258
        final double offset = moveAnimation.value.dy * size.height;
259
        if (offset < 0)
260 261
          return Rect.fromLTRB(0.0, size.height + offset, size.width, size.height);
        return Rect.fromLTRB(0.0, 0.0, size.width, offset);
262 263 264 265 266 267 268
    }
  }

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

  @override
269
  bool shouldReclip(_DismissibleClipper oldClipper) {
270 271 272 273 274
    return oldClipper.axis != axis
        || oldClipper.moveAnimation.value != moveAnimation.value;
  }
}

Ian Hickson's avatar
Ian Hickson committed
275 276
enum _FlingGestureKind { none, forward, reverse }

277
class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
278
  @override
279
  void initState() {
280
    super.initState();
281
    _moveController = AnimationController(duration: widget.movementDuration, vsync: this)
282 283
      ..addStatusListener(_handleDismissStatusChanged);
    _updateMoveAnimation();
284 285
  }

286 287
  AnimationController? _moveController;
  late Animation<Offset> _moveAnimation;
288

289 290
  AnimationController? _resizeController;
  Animation<double>? _resizeAnimation;
291 292 293

  double _dragExtent = 0.0;
  bool _dragUnderway = false;
294
  Size? _sizePriorToCollapse;
295

296 297 298
  @override
  bool get wantKeepAlive => _moveController?.isAnimating == true || _resizeController?.isAnimating == true;

299
  @override
300
  void dispose() {
301
    _moveController!.dispose();
302
    _resizeController?.dispose();
303 304 305
    super.dispose();
  }

306
  bool get _directionIsXAxis {
307 308 309
    return widget.direction == DismissDirection.horizontal
        || widget.direction == DismissDirection.endToStart
        || widget.direction == DismissDirection.startToEnd;
310 311
  }

312
  DismissDirection _extentToDirection(double extent) {
Ian Hickson's avatar
Ian Hickson committed
313
    if (extent == 0.0)
314
      return DismissDirection.none;
Ian Hickson's avatar
Ian Hickson committed
315
    if (_directionIsXAxis) {
316
      switch (Directionality.of(context)) {
Ian Hickson's avatar
Ian Hickson committed
317 318 319 320 321 322 323
        case TextDirection.rtl:
          return extent < 0 ? DismissDirection.startToEnd : DismissDirection.endToStart;
        case TextDirection.ltr:
          return extent > 0 ? DismissDirection.startToEnd : DismissDirection.endToStart;
      }
    }
    return extent > 0 ? DismissDirection.down : DismissDirection.up;
324 325
  }

326
  DismissDirection get _dismissDirection => _extentToDirection(_dragExtent);
327

328
  bool get _isActive {
329
    return _dragUnderway || _moveController!.isAnimating;
330 331
  }

332
  double get _overallDragAxisExtent {
333
    final Size size = context.size!;
334
    return _directionIsXAxis ? size.width : size.height;
335 336
  }

337
  void _handleDragStart(DragStartDetails details) {
338
    _dragUnderway = true;
339 340 341
    if (_moveController!.isAnimating) {
      _dragExtent = _moveController!.value * _overallDragAxisExtent * _dragExtent.sign;
      _moveController!.stop();
342 343
    } else {
      _dragExtent = 0.0;
344
      _moveController!.value = 0.0;
345
    }
346
    setState(() {
347
      _updateMoveAnimation();
348
    });
349 350
  }

351
  void _handleDragUpdate(DragUpdateDetails details) {
352
    if (!_isActive || _moveController!.isAnimating)
353
      return;
354

355
    final double delta = details.primaryDelta!;
356
    final double oldDragExtent = _dragExtent;
357
    switch (widget.direction) {
358 359
      case DismissDirection.horizontal:
      case DismissDirection.vertical:
360
        _dragExtent += delta;
361 362 363
        break;

      case DismissDirection.up:
364 365
        if (_dragExtent + delta < 0)
          _dragExtent += delta;
366 367 368
        break;

      case DismissDirection.down:
369 370
        if (_dragExtent + delta > 0)
          _dragExtent += delta;
371
        break;
Ian Hickson's avatar
Ian Hickson committed
372 373

      case DismissDirection.endToStart:
374
        switch (Directionality.of(context)) {
Ian Hickson's avatar
Ian Hickson committed
375 376 377 378 379 380 381 382 383 384 385 386
          case TextDirection.rtl:
            if (_dragExtent + delta > 0)
              _dragExtent += delta;
            break;
          case TextDirection.ltr:
            if (_dragExtent + delta < 0)
              _dragExtent += delta;
            break;
        }
        break;

      case DismissDirection.startToEnd:
387
        switch (Directionality.of(context)) {
Ian Hickson's avatar
Ian Hickson committed
388 389 390 391 392 393 394 395 396 397
          case TextDirection.rtl:
            if (_dragExtent + delta < 0)
              _dragExtent += delta;
            break;
          case TextDirection.ltr:
            if (_dragExtent + delta > 0)
              _dragExtent += delta;
            break;
        }
        break;
398 399 400 401

      case DismissDirection.none:
        _dragExtent = 0;
        break;
402
    }
403 404
    if (oldDragExtent.sign != _dragExtent.sign) {
      setState(() {
405
        _updateMoveAnimation();
406 407
      });
    }
408 409
    if (!_moveController!.isAnimating) {
      _moveController!.value = _dragExtent.abs() / _overallDragAxisExtent;
410 411 412 413
    }
  }

  void _updateMoveAnimation() {
414
    final double end = _dragExtent.sign;
415
    _moveAnimation = _moveController!.drive(
416 417 418
      Tween<Offset>(
        begin: Offset.zero,
        end: _directionIsXAxis
419 420
            ? Offset(end, widget.crossAxisEndOffset)
            : Offset(widget.crossAxisEndOffset, end),
421 422
      ),
    );
423 424
  }

Ian Hickson's avatar
Ian Hickson committed
425 426 427 428 429 430 431 432 433 434
  _FlingGestureKind _describeFlingGesture(Velocity velocity) {
    assert(widget.direction != null);
    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;
    }
435 436
    final double vx = velocity.pixelsPerSecond.dx;
    final double vy = velocity.pixelsPerSecond.dy;
437
    DismissDirection flingDirection;
Ian Hickson's avatar
Ian Hickson committed
438
    // Verify that the fling is in the generally right direction and fast enough.
439
    if (_directionIsXAxis) {
Ian Hickson's avatar
Ian Hickson committed
440 441 442 443
      if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta || vx.abs() < _kMinFlingVelocity)
        return _FlingGestureKind.none;
      assert(vx != 0.0);
      flingDirection = _extentToDirection(vx);
444
    } else {
Ian Hickson's avatar
Ian Hickson committed
445 446 447 448
      if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta || vy.abs() < _kMinFlingVelocity)
        return _FlingGestureKind.none;
      assert(vy != 0.0);
      flingDirection = _extentToDirection(vy);
449
    }
Ian Hickson's avatar
Ian Hickson committed
450 451 452 453
    assert(_dismissDirection != null);
    if (flingDirection == _dismissDirection)
      return _FlingGestureKind.forward;
    return _FlingGestureKind.reverse;
454 455
  }

456
  Future<void> _handleDragEnd(DragEndDetails details) async {
457
    if (!_isActive || _moveController!.isAnimating)
458
      return;
459
    _dragUnderway = false;
460
    if (_moveController!.isCompleted && await _confirmStartResizeAnimation() == true) {
461
      _startResizeAnimation();
Ian Hickson's avatar
Ian Hickson committed
462 463 464 465 466 467
      return;
    }
    final double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy;
    switch (_describeFlingGesture(details.velocity)) {
      case _FlingGestureKind.forward:
        assert(_dragExtent != 0.0);
468
        assert(!_moveController!.isDismissed);
Ian Hickson's avatar
Ian Hickson committed
469
        if ((widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold) >= 1.0) {
470
          _moveController!.reverse();
Ian Hickson's avatar
Ian Hickson committed
471 472 473
          break;
        }
        _dragExtent = flingVelocity.sign;
474
        _moveController!.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
Ian Hickson's avatar
Ian Hickson committed
475 476 477
        break;
      case _FlingGestureKind.reverse:
        assert(_dragExtent != 0.0);
478
        assert(!_moveController!.isDismissed);
Ian Hickson's avatar
Ian Hickson committed
479
        _dragExtent = flingVelocity.sign;
480
        _moveController!.fling(velocity: -flingVelocity.abs() * _kFlingVelocityScale);
Ian Hickson's avatar
Ian Hickson committed
481 482
        break;
      case _FlingGestureKind.none:
483 484 485
        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
486
          } else {
487
            _moveController!.reverse();
Ian Hickson's avatar
Ian Hickson committed
488 489 490
          }
        }
        break;
491
    }
492 493
  }

494 495 496 497 498
  Future<void> _handleDismissStatusChanged(AnimationStatus status) async {
    if (status == AnimationStatus.completed && !_dragUnderway) {
      if (await _confirmStartResizeAnimation() == true)
        _startResizeAnimation();
      else
499
        _moveController!.reverse();
500
    }
501
    updateKeepAlive();
502 503
  }

504
  Future<bool?> _confirmStartResizeAnimation() async {
505
    if (widget.confirmDismiss != null) {
506
      final DismissDirection direction = _dismissDirection;
507
      return widget.confirmDismiss!(direction);
508 509 510 511
    }
    return true;
  }

512 513
  void _startResizeAnimation() {
    assert(_moveController != null);
514
    assert(_moveController!.isCompleted);
515
    assert(_resizeController == null);
516
    assert(_sizePriorToCollapse == null);
517
    if (widget.resizeDuration == null) {
Ian Hickson's avatar
Ian Hickson committed
518
      if (widget.onDismissed != null) {
519
        final DismissDirection direction = _dismissDirection;
520
        widget.onDismissed!(direction);
Ian Hickson's avatar
Ian Hickson committed
521
      }
522
    } else {
523
      _resizeController = AnimationController(duration: widget.resizeDuration, vsync: this)
524 525
        ..addListener(_handleResizeProgressChanged)
        ..addStatusListener((AnimationStatus status) => updateKeepAlive());
526
      _resizeController!.forward();
527
      setState(() {
528
        _sizePriorToCollapse = context.size;
529
        _resizeAnimation = _resizeController!.drive(
530 531 532 533 534 535
          CurveTween(
            curve: _kResizeTimeCurve
          ),
        ).drive(
          Tween<double>(
            begin: 1.0,
536
            end: 0.0,
537 538
          ),
        );
539 540
      });
    }
541
  }
542

543
  void _handleResizeProgressChanged() {
544
    if (_resizeController!.isCompleted) {
Ian Hickson's avatar
Ian Hickson committed
545
      if (widget.onDismissed != null) {
546
        final DismissDirection direction = _dismissDirection;
547
        widget.onDismissed!(direction);
Ian Hickson's avatar
Ian Hickson committed
548
      }
549
    } else {
550
      if (widget.onResize != null)
551
        widget.onResize!();
552 553 554
    }
  }

555
  @override
556
  Widget build(BuildContext context) {
557
    super.build(context); // See AutomaticKeepAliveClientMixin.
Ian Hickson's avatar
Ian Hickson committed
558 559 560

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

561
    Widget? background = widget.background;
562
    if (widget.secondaryBackground != null) {
563
      final DismissDirection direction = _dismissDirection;
564
      if (direction == DismissDirection.endToStart || direction == DismissDirection.up)
565
        background = widget.secondaryBackground;
566 567
    }

568 569 570
    if (_resizeAnimation != null) {
      // we've been dragged aside, and are now resizing.
      assert(() {
571 572
        if (_resizeAnimation!.status != AnimationStatus.forward) {
          assert(_resizeAnimation!.status == AnimationStatus.completed);
573 574 575 576 577 578 579
          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 '
              'widget from the application once that handler has fired.'
            )
          ]);
580 581
        }
        return true;
582
      }());
583

584
      return SizeTransition(
585
        sizeFactor: _resizeAnimation!,
586
        axis: _directionIsXAxis ? Axis.vertical : Axis.horizontal,
587
        child: SizedBox(
588 589
          width: _sizePriorToCollapse!.width,
          height: _sizePriorToCollapse!.height,
590 591
          child: background,
        ),
592
      );
Adam Barth's avatar
Adam Barth committed
593
    }
594

595
    Widget content = SlideTransition(
596
      position: _moveAnimation,
597
      child: widget.child,
598
    );
599

600
    if (background != null) {
601 602 603 604 605 606 607 608 609
      content = Stack(children: <Widget>[
        if (!_moveAnimation.isDismissed)
          Positioned.fill(
            child: ClipRect(
              clipper: _DismissibleClipper(
                axis: _directionIsXAxis ? Axis.horizontal : Axis.vertical,
                moveAnimation: _moveAnimation,
              ),
              child: background,
610
            ),
611
          ),
612 613
        content,
      ]);
614
    }
615
    // We are not resizing but we may be being dragging in widget.direction.
616
    return GestureDetector(
617 618 619 620 621 622
      onHorizontalDragStart: _directionIsXAxis ? _handleDragStart : null,
      onHorizontalDragUpdate: _directionIsXAxis ? _handleDragUpdate : null,
      onHorizontalDragEnd: _directionIsXAxis ? _handleDragEnd : null,
      onVerticalDragStart: _directionIsXAxis ? null : _handleDragStart,
      onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate,
      onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd,
623
      behavior: widget.behavior,
624 625
      child: content,
      dragStartBehavior: widget.dragStartBehavior,
626 627 628
    );
  }
}