130 lines
3.9 KiB
Dart
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),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|