You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 00:38:11