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