一、需求来源
工作中遇到可拖动悬浮按钮的需求,顺手封装一个组件 NSuspension。核心原理是 Stack 拖动的同时不断更新 Postioned 位置。效果下:
二、使用示例
buildBody() {
return NSuspension(
padding: EdgeInsets.only(left: 20, top: 30, right: 40, bottom: 50),
childSize: Size(80, 80),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.all(Radius.circular(8.w)),
),
),
bgChild: Container(
color: Colors.black.withOpacity(0.1), //Color.fromRGBO(242, 243, 248, 1),
),
);
}
二、NSuspension 组件源码
import 'package:flutter/material.dart';
/// 堆代码 duidaima.com
/// 悬浮组件
class NSuspension extends StatefulWidget {
NSuspension({
Key? key,
required this.child,
required this.bgChild,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.clipBehavior = Clip.hardEdge,
this.padding = EdgeInsets.zero,
this.childSize = const Size(100, 100),
}) : super(key: key);
final AlignmentGeometry alignment;
final TextDirection? textDirection;
final StackFit fit;
final Clip clipBehavior;
/// 底部组件
final Widget bgChild;
/// 悬浮组件
final Widget child;
/// 悬浮组件宽高
final Size childSize;
/// 距离四周边界
final EdgeInsets padding;
@override
_NSuspensionState createState() => _NSuspensionState();
}
class _NSuspensionState extends State<NSuspension> {
final _topVN = ValueNotifier(0.0);
final _leftVN = ValueNotifier(0.0);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints){
return Stack(
alignment: widget.alignment,
textDirection: widget.textDirection,
fit: widget.fit,
clipBehavior: widget.clipBehavior,
children: [
widget.bgChild,
buildSuspension(
child: widget.child,
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight,
),
],
);
}
);
}
buildSuspension({
required Widget child,
required double maxWidth,
required double maxHeight
}) {
return AnimatedBuilder(
animation: Listenable.merge([
_topVN,
_leftVN,
]),
child: child,
builder: (context, child) {
return Positioned(
top: _topVN.value,
left: _leftVN.value,
child: GestureDetector(
onTap: () {
debugPrint("onTap");
},
onPanUpdate: (DragUpdateDetails e) {
// debugPrint("e.delta:${e.delta.dx},${e.delta.dy}");
//用户手指滑动时,更新偏移,重新构建
//顶部
if (_topVN.value < widget.padding.top && e.delta.dy < 0) {
return;
}
// 左边
if (_leftVN.value < widget.padding.left && e.delta.dx < 0) {
return;
}
// 右边
if (_topVN.value > (maxHeight - widget.childSize.height - widget.padding.bottom) && e.delta.dy > 0) {
return;
}
// 下边
if (_leftVN.value > (maxWidth - widget.childSize.width - widget.padding.right) && e.delta.dx > 0) {
return;
}
_topVN.value += e.delta.dy;
_leftVN.value += e.delta.dx;
// debugPrint("xy:${_leftVN.value},${_topVN.value}");
},
child: child,
)
);
}
);
}
}
总结
1、此组件封装的核心是通过 AnimatedBuilder 多项监听,拖动同时不断更新 top、left 进而实现拖动效果;
2、此组件不完美之处需要传入悬浮按钮的宽高 childSize,暂时没想到有什么完美的方法可以自动获取到。有知道的同学可以发消息,一起进步,共同成长。