如何实现Flutter中GIF所示的可滑动Dismissible动画效果?
实现匹配目标效果的自定义滑动删除组件
针对你需要的滑动动画效果,原生Dismissible组件无法完全满足,我们可以通过自定义手势监听+动画控制器来实现,以下是完整实现方案:
核心实现逻辑
- 用
GestureDetector监听水平滑动手势,实时控制列表项的位移 - 借助
AnimationController和多组动画,实现列表项滑动到阈值后的收缩、淡出动画 - 采用
Stack布局,底层放置操作按钮,上层是可滑动的列表内容项
完整代码实现
import 'package:flutter/material.dart'; class CustomSwipeDismissible extends StatefulWidget { final Widget child; final Widget actionWidget; final VoidCallback onDismissed; final double swipeThreshold; // 触发删除的滑动比例阈值 const CustomSwipeDismissible({ super.key, required this.child, required this.actionWidget, required this.onDismissed, this.swipeThreshold = 0.3, }); @override State<CustomSwipeDismissible> createState() => _CustomSwipeDismissibleState(); } class _CustomSwipeDismissibleState extends State<CustomSwipeDismissible> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation<double> _slideAnimation; late Animation<double> _scaleAnimation; late Animation<double> _opacityAnimation; double _dragOffset = 0.0; final double _maxSlideDistance = 80.0; // 对应操作按钮的宽度 @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); // 配置动画参数,匹配目标效果的收缩+淡出 _slideAnimation = Tween<double>(begin: 0, end: -_maxSlideDistance).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); _scaleAnimation = Tween<double>(begin: 1, end: 0.8).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeIn), ); _opacityAnimation = Tween<double>(begin: 1, end: 0).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeOut), ); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _onDragUpdate(DragUpdateDetails details) { // 限制滑动范围,仅允许左滑且不超过操作按钮宽度 setState(() { _dragOffset += details.delta.dx; if (_dragOffset > 0) _dragOffset = 0; if (_dragOffset < -_maxSlideDistance) _dragOffset = -_maxSlideDistance; }); } void _onDragEnd(DragEndDetails details) { final dismissRatio = (-_dragOffset) / _maxSlideDistance; // 判断是否达到删除阈值,执行对应动画 if (dismissRatio >= widget.swipeThreshold) { _animationController.forward().then((_) { widget.onDismissed(); _animationController.reset(); _dragOffset = 0; }); } else { setState(() { _dragOffset = 0; }); } } @override Widget build(BuildContext context) { return Stack( children: [ // 底层固定显示的操作按钮 Align( alignment: Alignment.centerRight, child: widget.actionWidget, ), // 上层可滑动的列表内容项 GestureDetector( onHorizontalDragUpdate: _onDragUpdate, onHorizontalDragEnd: _onDragEnd, child: AnimatedBuilder( animation: Listenable.merge([_animationController, _dragOffset]), builder: (context, child) { // 区分实时滑动和触发删除后的动画状态 final currentOffset = _animationController.isAnimating ? _slideAnimation.value : _dragOffset; final currentScale = _animationController.isAnimating ? _scaleAnimation.value : 1.0; final currentOpacity = _animationController.isAnimating ? _opacityAnimation.value : 1.0; return Transform( transform: Matrix4.translationValues(currentOffset, 0, 0) ..scale(currentScale, currentScale), alignment: Alignment.centerRight, child: Opacity( opacity: currentOpacity, child: child, ), ); }, child: widget.child, ), ), ], ); } } // 使用示例页面 class SwipeDemoPage extends StatefulWidget { const SwipeDemoPage({super.key}); @override State<SwipeDemoPage> createState() => _SwipeDemoPageState(); } class _SwipeDemoPageState extends State<SwipeDemoPage> { final List<String> _items = List.generate(10, (index) => "列表项 ${index + 1}"); void _removeItem(int index) { setState(() { _items.removeAt(index); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("自定义滑动删除示例")), body: ListView.builder( itemCount: _items.length, itemBuilder: (context, index) { return CustomSwipeDismissible( actionWidget: Container( width: 80, color: Colors.red, alignment: Alignment.center, child: const Icon(Icons.delete, color: Colors.white), ), onDismissed: () => _removeItem(index), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: Colors.grey.shade200)), color: Colors.white, ), child: Text(_items[index], style: const TextStyle(fontSize: 16)), ), ); }, ), ); } }
效果调整说明
- 调整
_maxSlideDistance可以匹配操作按钮的实际宽度 - 修改
swipeThreshold可以改变触发删除的滑动距离比例 - 调整
AnimationController的duration和动画曲线(Curves),可以微调动画的快慢和节奏,完全匹配目标GIF的效果
内容的提问来源于stack exchange,提问作者Farid Ahmadov
相关产品推荐
相关产品推荐

