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