Files
2026-09-15 15:44:13 +07:00

130 lines
3.9 KiB
Dart

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<SwipeActionItem> createState() => _SwipeActionItemState();
}
/// 全局只允许一项展开:展开新的会自动收起旧的
_SwipeActionItemState? _opened;
class _SwipeActionItemState extends State<SwipeActionItem> 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),
),
),
),
);
}
}