import 'package:flutter/material.dart'; import 'package:get/get.dart'; /// 左滑露出操作按钮的列表项包装(如「我的关注」左滑取消关注)。 /// /// 不用 Dismissible:那个滑到底即执行,取关这种不可撤销的操作误触代价太大, /// 这里做成「左滑露出按钮、点按钮才执行」。 class SwipeActionItem extends StatefulWidget { final Widget child; final String actionText; final VoidCallback onAction; final Color actionColor; final double actionWidth; /// 滑动层的底色。列表项自身多半是透明的,不垫一层实色,底下的按钮会直接透上来 final Color? backgroundColor; const SwipeActionItem({ super.key, required this.child, required this.actionText, required this.onAction, this.actionColor = const Color(0xffE03017), this.actionWidth = 88, this.backgroundColor, }); /// 关掉当前展开的那一项(列表滚动时调,避免滑走了还留着一个张开的) static void closeOpened() => _opened?._close(); @override State createState() => _SwipeActionItemState(); } /// 全局只允许一项展开:展开新的会自动收起旧的 _SwipeActionItemState? _opened; class _SwipeActionItemState extends State with SingleTickerProviderStateMixin { late final AnimationController _ctr = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), ); bool get _isOpen => _ctr.value > 0; @override void dispose() { if (_opened == this) _opened = null; _ctr.dispose(); super.dispose(); } void _open() { if (_opened != null && _opened != this) _opened!._close(); _opened = this; _ctr.forward(); } void _close() { if (_opened == this) _opened = null; if (mounted) _ctr.reverse(); } void _onDragUpdate(DragUpdateDetails d) { // 手指往左走 primaryDelta 为负,换算成 0~1 的展开进度 _ctr.value -= (d.primaryDelta ?? 0) / widget.actionWidth; } void _onDragEnd(DragEndDetails d) { final v = d.primaryVelocity ?? 0; if (v < -300) return _open(); // 甩一下就展开,不看位置 if (v > 300) return _close(); _ctr.value > 0.5 ? _open() : _close(); } @override Widget build(BuildContext context) { return GestureDetector( onHorizontalDragUpdate: _onDragUpdate, onHorizontalDragEnd: _onDragEnd, child: AnimatedBuilder( animation: _ctr, builder: (_, child) { return Stack( children: [ // 按钮垫在底下,靠内容左移露出来;跟着内容一起裁,避免收起时露边 Positioned.fill(child: _actionButton()), Transform.translate( offset: Offset(-widget.actionWidth * _ctr.value, 0), child: ColoredBox( color: widget.backgroundColor ?? Get.theme.scaffoldBackgroundColor, // 展开状态下先吞掉一次点击用于收起,别直接把用户点进详情页 child: _isOpen ? GestureDetector( onTap: _close, child: AbsorbPointer(child: child), ) : child, ), ), ], ); }, child: widget.child, ), ); } Widget _actionButton() { return Align( alignment: Alignment.centerRight, child: GestureDetector( onTap: () { _close(); widget.onAction(); }, child: Container( width: widget.actionWidth, alignment: Alignment.center, color: widget.actionColor, child: Text( widget.actionText, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), ), ), ); } }