import 'dart:math' as math; import 'package:flutter/material.dart'; import 'stream_chat_theme.dart'; /// class Swipeable extends StatefulWidget { final Widget child; final Widget backgroundIcon; final VoidCallback onSwipeStart; final VoidCallback onSwipeCancel; final VoidCallback onSwipeEnd; final double threshold; /// const Swipeable({ @required this.child, @required this.backgroundIcon, this.onSwipeStart, this.onSwipeCancel, this.onSwipeEnd, this.threshold = 82.0, }); @override State createState() => _SwipeableState(); } class _SwipeableState extends State with TickerProviderStateMixin { double _dragExtent = 0.0; AnimationController _moveController; AnimationController _iconMoveController; Animation _moveAnimation; Animation _iconTransitionAnimation; Animation _iconFadeAnimation; bool _pastThreshold = false; final _animationDuration = const Duration(milliseconds: 200); @override void initState() { super.initState(); _moveController = AnimationController(duration: _animationDuration, vsync: this); _iconMoveController = AnimationController(duration: _animationDuration, vsync: this); _moveAnimation = Tween(begin: Offset.zero, end: Offset(1.0, 0.0)) .animate(_moveController); _iconTransitionAnimation = Tween(begin: Offset(-0.1, 0.0), end: Offset(0.4, 0.0)) .animate(_moveController); _iconFadeAnimation = Tween(begin: 0.7, end: 1.0).animate(_iconMoveController); final controllerValue = 0.0; _moveController.animateTo(controllerValue); _iconMoveController.animateTo(controllerValue); } @override void dispose() { _moveController.dispose(); _iconMoveController.dispose(); super.dispose(); } void _handleDragStart(DragStartDetails details) { if (widget.onSwipeStart != null) { widget.onSwipeStart(); } } void _handleDragUpdate(DragUpdateDetails details) { final delta = details.primaryDelta; _dragExtent += delta; if (_dragExtent.isNegative) return; var movePastThresholdPixels = widget.threshold; var newPos = _dragExtent.abs() / context.size.width; if (_dragExtent.abs() > movePastThresholdPixels) { // how many "thresholds" past the threshold we are. 1 = the threshold 2 // = two thresholds. var n = _dragExtent.abs() / movePastThresholdPixels; // Take the number of thresholds past the threshold, and reduce this // number var reducedThreshold = math.pow(n, 0.3); var adjustedPixelPos = movePastThresholdPixels * reducedThreshold; newPos = adjustedPixelPos / context.size.width; if (_dragExtent > 0 && !_pastThreshold) { _iconMoveController.value = 1; _pastThreshold = true; } } else { // Send a cancel event if the user has swiped back underneath the // threshold if (_pastThreshold && widget.onSwipeCancel != null) { widget.onSwipeCancel(); } _pastThreshold = false; } if (!_pastThreshold || newPos < _moveController.value) { _iconMoveController.value = newPos; } _moveController.value = newPos; } void _handleDragEnd(DragEndDetails details) { _moveController.animateTo(0.0, duration: _animationDuration); _iconMoveController.animateTo(0.0, duration: _animationDuration); _dragExtent = 0.0; if (_pastThreshold && widget.onSwipeEnd != null) { widget.onSwipeEnd(); } } @override Widget build(BuildContext context) { return GestureDetector( onHorizontalDragStart: _handleDragStart, onHorizontalDragUpdate: _handleDragUpdate, onHorizontalDragEnd: _handleDragEnd, behavior: HitTestBehavior.opaque, child: Stack( alignment: Alignment.center, fit: StackFit.passthrough, children: [ SlideTransition( position: _iconTransitionAnimation, child: Row( children: [ FadeTransition( opacity: _iconFadeAnimation, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: StreamChatTheme.of(context) .colorTheme .greyGainsboro, ), ), child: widget.backgroundIcon, ), ), ], ), ), SlideTransition( position: _moveAnimation, child: widget.child, ), ], ), ); } }