1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'automatic_keep_alive.dart';
import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
const Curve _kResizeTimeCurve = Interval(0.4, 1.0, curve: Curves.ease);
const double _kMinFlingVelocity = 700.0;
const double _kMinFlingVelocityDelta = 400.0;
const double _kFlingVelocityScale = 1.0 / 300.0;
const double _kDismissThreshold = 0.4;
/// Signature used by [Dismissible] to indicate that it has been dismissed in
/// the given `direction`.
///
/// Used by [Dismissible.onDismissed].
typedef DismissDirectionCallback = void Function(DismissDirection direction);
/// Signature used by [Dismissible] to give the application an opportunity to
/// confirm or veto a dismiss gesture.
///
/// Used by [Dismissible.confirmDismiss].
typedef ConfirmDismissCallback = Future<bool?> Function(DismissDirection direction);
/// The direction in which a [Dismissible] can be dismissed.
enum DismissDirection {
/// The [Dismissible] can be dismissed by dragging either up or down.
vertical,
/// The [Dismissible] can be dismissed by dragging either left or right.
horizontal,
/// The [Dismissible] can be dismissed by dragging in the reverse of the
/// reading direction (e.g., from right to left in left-to-right languages).
endToStart,
/// The [Dismissible] can be dismissed by dragging in the reading direction
/// (e.g., from left to right in left-to-right languages).
startToEnd,
/// The [Dismissible] can be dismissed by dragging up only.
up,
/// The [Dismissible] can be dismissed by dragging down only.
down,
/// The [Dismissible] cannot be dismissed by dragging.
none
}
/// A widget that can be dismissed by dragging in the indicated [direction].
///
/// Dragging or flinging this widget in the [DismissDirection] causes the child
/// to slide out of view. Following the slide animation, if [resizeDuration] is
/// non-null, the Dismissible widget animates its height (or width, whichever is
/// perpendicular to the dismiss direction) to zero over the [resizeDuration].
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=iEMgjrfuc58}
///
/// {@tool dartpad --template=stateful_widget_scaffold}
///
/// This sample shows how you can use the [Dismissible] widget to
/// remove list items using swipe gestures. Swipe any of the list
/// tiles to the left or right to dismiss them from the [ListView].
///
/// ```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}
///
/// Backgrounds can be used to implement the "leave-behind" idiom. If a background
/// is specified it is stacked behind the Dismissible's child and is exposed when
/// the child moves.
///
/// The widget calls the [onDismissed] callback either after its size has
/// collapsed to zero (if [resizeDuration] is non-null) or immediately after
/// the slide animation (if [resizeDuration] is null). If the Dismissible is a
/// 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.
class Dismissible extends StatefulWidget {
/// Creates a widget that can be dismissed.
///
/// The [key] argument must not be null 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.
const Dismissible({
required Key key,
required this.child,
this.background,
this.secondaryBackground,
this.confirmDismiss,
this.onResize,
this.onDismissed,
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,
this.dragStartBehavior = DragStartBehavior.start,
this.behavior = HitTestBehavior.opaque,
}) : assert(key != null),
assert(secondaryBackground == null || background != null),
assert(dragStartBehavior != null),
super(key: key);
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
/// 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.
final Widget? background;
/// 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.
final Widget? secondaryBackground;
/// 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.
///
/// If the returned Future<bool?> completes to false or null the [onResize]
/// and [onDismissed] callbacks will not run.
final ConfirmDismissCallback? confirmDismiss;
/// Called when the widget changes size (i.e., when contracting before being dismissed).
final VoidCallback? onResize;
/// Called when the widget has been dismissed, after finishing resizing.
final DismissDirectionCallback? onDismissed;
/// The direction in which the widget can be dismissed.
final DismissDirection direction;
/// 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
/// immediately after the widget is dismissed.
final Duration? resizeDuration;
/// The offset threshold the item has to be dragged in order to be considered
/// dismissed.
///
/// 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.
///
/// 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.
final Map<DismissDirection, double> dismissThresholds;
/// 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;
/// 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.
///
/// By default, the drag start behavior is [DragStartBehavior.start].
///
/// See also:
///
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
final DragStartBehavior dragStartBehavior;
/// How to behave during hit tests.
///
/// This defaults to [HitTestBehavior.opaque].
final HitTestBehavior behavior;
@override
_DismissibleState createState() => _DismissibleState();
}
class _DismissibleClipper extends CustomClipper<Rect> {
_DismissibleClipper({
required this.axis,
required this.moveAnimation,
}) : assert(axis != null),
assert(moveAnimation != null),
super(reclip: moveAnimation);
final Axis axis;
final Animation<Offset> moveAnimation;
@override
Rect getClip(Size size) {
assert(axis != null);
switch (axis) {
case Axis.horizontal:
final double offset = moveAnimation.value.dx * size.width;
if (offset < 0)
return Rect.fromLTRB(size.width + offset, 0.0, size.width, size.height);
return Rect.fromLTRB(0.0, 0.0, offset, size.height);
case Axis.vertical:
final double offset = moveAnimation.value.dy * size.height;
if (offset < 0)
return Rect.fromLTRB(0.0, size.height + offset, size.width, size.height);
return Rect.fromLTRB(0.0, 0.0, size.width, offset);
}
}
@override
Rect getApproximateClipRect(Size size) => getClip(size);
@override
bool shouldReclip(_DismissibleClipper oldClipper) {
return oldClipper.axis != axis
|| oldClipper.moveAnimation.value != moveAnimation.value;
}
}
enum _FlingGestureKind { none, forward, reverse }
class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
@override
void initState() {
super.initState();
_moveController = AnimationController(duration: widget.movementDuration, vsync: this)
..addStatusListener(_handleDismissStatusChanged);
_updateMoveAnimation();
}
AnimationController? _moveController;
late Animation<Offset> _moveAnimation;
AnimationController? _resizeController;
Animation<double>? _resizeAnimation;
double _dragExtent = 0.0;
bool _dragUnderway = false;
Size? _sizePriorToCollapse;
@override
bool get wantKeepAlive => _moveController?.isAnimating == true || _resizeController?.isAnimating == true;
@override
void dispose() {
_moveController!.dispose();
_resizeController?.dispose();
super.dispose();
}
bool get _directionIsXAxis {
return widget.direction == DismissDirection.horizontal
|| widget.direction == DismissDirection.endToStart
|| widget.direction == DismissDirection.startToEnd;
}
DismissDirection _extentToDirection(double extent) {
if (extent == 0.0)
return DismissDirection.none;
if (_directionIsXAxis) {
switch (Directionality.of(context)) {
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;
}
DismissDirection get _dismissDirection => _extentToDirection(_dragExtent);
bool get _isActive {
return _dragUnderway || _moveController!.isAnimating;
}
double get _overallDragAxisExtent {
final Size size = context.size!;
return _directionIsXAxis ? size.width : size.height;
}
void _handleDragStart(DragStartDetails details) {
_dragUnderway = true;
if (_moveController!.isAnimating) {
_dragExtent = _moveController!.value * _overallDragAxisExtent * _dragExtent.sign;
_moveController!.stop();
} else {
_dragExtent = 0.0;
_moveController!.value = 0.0;
}
setState(() {
_updateMoveAnimation();
});
}
void _handleDragUpdate(DragUpdateDetails details) {
if (!_isActive || _moveController!.isAnimating)
return;
final double delta = details.primaryDelta!;
final double oldDragExtent = _dragExtent;
switch (widget.direction) {
case DismissDirection.horizontal:
case DismissDirection.vertical:
_dragExtent += delta;
break;
case DismissDirection.up:
if (_dragExtent + delta < 0)
_dragExtent += delta;
break;
case DismissDirection.down:
if (_dragExtent + delta > 0)
_dragExtent += delta;
break;
case DismissDirection.endToStart:
switch (Directionality.of(context)) {
case TextDirection.rtl:
if (_dragExtent + delta > 0)
_dragExtent += delta;
break;
case TextDirection.ltr:
if (_dragExtent + delta < 0)
_dragExtent += delta;
break;
}
break;
case DismissDirection.startToEnd:
switch (Directionality.of(context)) {
case TextDirection.rtl:
if (_dragExtent + delta < 0)
_dragExtent += delta;
break;
case TextDirection.ltr:
if (_dragExtent + delta > 0)
_dragExtent += delta;
break;
}
break;
case DismissDirection.none:
_dragExtent = 0;
break;
}
if (oldDragExtent.sign != _dragExtent.sign) {
setState(() {
_updateMoveAnimation();
});
}
if (!_moveController!.isAnimating) {
_moveController!.value = _dragExtent.abs() / _overallDragAxisExtent;
}
}
void _updateMoveAnimation() {
final double end = _dragExtent.sign;
_moveAnimation = _moveController!.drive(
Tween<Offset>(
begin: Offset.zero,
end: _directionIsXAxis
? Offset(end, widget.crossAxisEndOffset)
: Offset(widget.crossAxisEndOffset, end),
),
);
}
_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;
}
final double vx = velocity.pixelsPerSecond.dx;
final double vy = velocity.pixelsPerSecond.dy;
DismissDirection flingDirection;
// Verify that the fling is in the generally right direction and fast enough.
if (_directionIsXAxis) {
if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta || vx.abs() < _kMinFlingVelocity)
return _FlingGestureKind.none;
assert(vx != 0.0);
flingDirection = _extentToDirection(vx);
} else {
if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta || vy.abs() < _kMinFlingVelocity)
return _FlingGestureKind.none;
assert(vy != 0.0);
flingDirection = _extentToDirection(vy);
}
assert(_dismissDirection != null);
if (flingDirection == _dismissDirection)
return _FlingGestureKind.forward;
return _FlingGestureKind.reverse;
}
Future<void> _handleDragEnd(DragEndDetails details) async {
if (!_isActive || _moveController!.isAnimating)
return;
_dragUnderway = false;
if (_moveController!.isCompleted && await _confirmStartResizeAnimation() == true) {
_startResizeAnimation();
return;
}
final double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy;
switch (_describeFlingGesture(details.velocity)) {
case _FlingGestureKind.forward:
assert(_dragExtent != 0.0);
assert(!_moveController!.isDismissed);
if ((widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold) >= 1.0) {
_moveController!.reverse();
break;
}
_dragExtent = flingVelocity.sign;
_moveController!.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
break;
case _FlingGestureKind.reverse:
assert(_dragExtent != 0.0);
assert(!_moveController!.isDismissed);
_dragExtent = flingVelocity.sign;
_moveController!.fling(velocity: -flingVelocity.abs() * _kFlingVelocityScale);
break;
case _FlingGestureKind.none:
if (!_moveController!.isDismissed) { // we already know it's not completed, we check that above
if (_moveController!.value > (widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold)) {
_moveController!.forward();
} else {
_moveController!.reverse();
}
}
break;
}
}
Future<void> _handleDismissStatusChanged(AnimationStatus status) async {
if (status == AnimationStatus.completed && !_dragUnderway) {
if (await _confirmStartResizeAnimation() == true)
_startResizeAnimation();
else
_moveController!.reverse();
}
updateKeepAlive();
}
Future<bool?> _confirmStartResizeAnimation() async {
if (widget.confirmDismiss != null) {
final DismissDirection direction = _dismissDirection;
return widget.confirmDismiss!(direction);
}
return true;
}
void _startResizeAnimation() {
assert(_moveController != null);
assert(_moveController!.isCompleted);
assert(_resizeController == null);
assert(_sizePriorToCollapse == null);
if (widget.resizeDuration == null) {
if (widget.onDismissed != null) {
final DismissDirection direction = _dismissDirection;
widget.onDismissed!(direction);
}
} else {
_resizeController = AnimationController(duration: widget.resizeDuration, vsync: this)
..addListener(_handleResizeProgressChanged)
..addStatusListener((AnimationStatus status) => updateKeepAlive());
_resizeController!.forward();
setState(() {
_sizePriorToCollapse = context.size;
_resizeAnimation = _resizeController!.drive(
CurveTween(
curve: _kResizeTimeCurve
),
).drive(
Tween<double>(
begin: 1.0,
end: 0.0,
),
);
});
}
}
void _handleResizeProgressChanged() {
if (_resizeController!.isCompleted) {
if (widget.onDismissed != null) {
final DismissDirection direction = _dismissDirection;
widget.onDismissed!(direction);
}
} else {
if (widget.onResize != null)
widget.onResize!();
}
}
@override
Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin.
assert(!_directionIsXAxis || debugCheckHasDirectionality(context));
Widget? background = widget.background;
if (widget.secondaryBackground != null) {
final DismissDirection direction = _dismissDirection;
if (direction == DismissDirection.endToStart || direction == DismissDirection.up)
background = widget.secondaryBackground;
}
if (_resizeAnimation != null) {
// we've been dragged aside, and are now resizing.
assert(() {
if (_resizeAnimation!.status != AnimationStatus.forward) {
assert(_resizeAnimation!.status == AnimationStatus.completed);
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.'
)
]);
}
return true;
}());
return SizeTransition(
sizeFactor: _resizeAnimation!,
axis: _directionIsXAxis ? Axis.vertical : Axis.horizontal,
child: SizedBox(
width: _sizePriorToCollapse!.width,
height: _sizePriorToCollapse!.height,
child: background,
),
);
}
Widget content = SlideTransition(
position: _moveAnimation,
child: widget.child,
);
if (background != null) {
content = Stack(children: <Widget>[
if (!_moveAnimation.isDismissed)
Positioned.fill(
child: ClipRect(
clipper: _DismissibleClipper(
axis: _directionIsXAxis ? Axis.horizontal : Axis.vertical,
moveAnimation: _moveAnimation,
),
child: background,
),
),
content,
]);
}
// We are not resizing but we may be being dragging in widget.direction.
return GestureDetector(
onHorizontalDragStart: _directionIsXAxis ? _handleDragStart : null,
onHorizontalDragUpdate: _directionIsXAxis ? _handleDragUpdate : null,
onHorizontalDragEnd: _directionIsXAxis ? _handleDragEnd : null,
onVerticalDragStart: _directionIsXAxis ? null : _handleDragStart,
onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate,
onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd,
behavior: widget.behavior,
child: content,
dragStartBehavior: widget.dragStartBehavior,
);
}
}