import 'package:flutter/material.dart'; /// A widget used to dismiss its [child]. /// /// Similar to [Dismissible] with some adjustments. class CustomDismissible extends StatefulWidget { const CustomDismissible({ required this.child, this.onDismissed, this.dismissThreshold = 0.2, this.enabled = true, Key? key, }) : super(key: key); final Widget child; final double dismissThreshold; final VoidCallback? onDismissed; final bool enabled; @override State createState() => _CustomDismissibleState(); } class _CustomDismissibleState extends State with SingleTickerProviderStateMixin { late AnimationController _animateController; late Animation _moveAnimation; late Animation _scaleAnimation; late Animation _opacityAnimation; double _dragExtent = 0; bool _dragUnderway = false; bool get _isActive => _dragUnderway || _animateController.isAnimating; @override void initState() { super.initState(); _animateController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _updateMoveAnimation(); } @override void dispose() { _animateController.dispose(); super.dispose(); } void _updateMoveAnimation() { final double end = _dragExtent.sign; _moveAnimation = _animateController.drive( Tween( begin: Offset.zero, end: Offset(0, end), ), ); _scaleAnimation = _animateController.drive(Tween( begin: 1, end: 0.5, )); _opacityAnimation = DecorationTween( begin: const BoxDecoration(color: Color(0xFF000000)), end: const BoxDecoration(color: Color(0x00000000)), ).animate(_animateController); } void _handleDragStart(DragStartDetails details) { _dragUnderway = true; if (_animateController.isAnimating) { _dragExtent = _animateController.value * context.size!.height * _dragExtent.sign; _animateController.stop(); } else { _dragExtent = 0.0; _animateController.value = 0.0; } setState(_updateMoveAnimation); } void _handleDragUpdate(DragUpdateDetails details) { if (!_isActive || _animateController.isAnimating) { return; } final double delta = details.primaryDelta!; final double oldDragExtent = _dragExtent; if (_dragExtent + delta < 0) { _dragExtent += delta; } else if (_dragExtent + delta > 0) { _dragExtent += delta; } if (oldDragExtent.sign != _dragExtent.sign) { setState(_updateMoveAnimation); } if (!_animateController.isAnimating) { _animateController.value = _dragExtent.abs() / context.size!.height; } } void _handleDragEnd(DragEndDetails details) { if (!_isActive || _animateController.isAnimating) { return; } _dragUnderway = false; if (_animateController.isCompleted) { return; } if (!_animateController.isDismissed) { // if the dragged value exceeded the dismissThreshold, call onDismissed // else animate back to initial position. if (_animateController.value > widget.dismissThreshold) { widget.onDismissed?.call(); } else { _animateController.reverse(); } } } @override Widget build(BuildContext context) { final Widget content = DecoratedBoxTransition( decoration: _opacityAnimation, child: SlideTransition( position: _moveAnimation, child: ScaleTransition( scale: _scaleAnimation, child: widget.child, ), ), ); return GestureDetector( behavior: HitTestBehavior.translucent, onVerticalDragStart: widget.enabled ? _handleDragStart : null, onVerticalDragUpdate: widget.enabled ? _handleDragUpdate : null, onVerticalDragEnd: widget.enabled ? _handleDragEnd : null, child: content, ); } }