Flutter 允许您使用FloatingActionButton
小部件添加浮动操作按钮。但是,它不允许您拖动按钮。如果你想让它可拖动怎么办。本教程有一个示例,说明您需要做什么才能创建浮动操作按钮,只要它位于父小部件内,就可以将其拖动到屏幕周围的任何位置。
创建可拖动的浮动操作按钮
我们将为这样的小部件创建一个类。我们需要处理的第一件事是使按钮可跟随指针拖动的能力。可以使用的小部件之一是Listener
,它能够检测指针移动事件并提供移动细节。基本上,按钮需要包装为Listener
.
该Listener
小部件具有onPointerMove
可用于反馈当指针移动时的事件,这将被称为参数。回调函数必须有一个参数PointerMoveEvent
,其中包含 x 和 y 方向(delta.dx
和delta.dy
)的移动增量。必须根据移动增量更新按钮的偏移量。
一个浮动的动作按钮通常可以在点击时执行一个动作,所以我们添加一个名为onPressed
( VoidCallback
) 的参数作为参数。该Listener
部件有onPointerUp
参数当用户释放的指针将被调用。因此,我们可以使用它来传递调用onPressed
回调的回调函数。但你需要小心。通常,所需的行为是onPressed
仅在点击按钮时调用回调,而不是在拖动结束时调用。然而,当拖动结束时,指针向上事件也会被触发。作为解决方案,我们需要跟踪按钮是否被拖动。该_isDragging
状态变量是为此目的而设立。它应该更新到true
指针移动时。所以,我们可以检查内部onPointerUp
callback 仅onPressed
在值为_isDragging
is 时调用回调false
。
下面是用于创建可拖动浮动操作按钮的类。它有一些参数,包括child
(要设置为按钮的小部件)、initialOffset
(移动前的初始偏移量)和onPressed
(单击按钮时调用的回调)。child
小部件使用Positioned
基于当前偏移量的小部件呈现。它也被包装为Listener
小部件的子级。还有一种方法_updatePosition
可以根据移动增量更新当前偏移量。
class DraggableFloatingActionButton extends StatefulWidget {
final Widget child;
final Offset initialOffset;
final VoidCallback onPressed;
DraggableFloatingActionButton({
required this.child,
required this.initialOffset,
required this.onPressed,
});
@override
State<StatefulWidget> createState() => _DraggableFloatingActionButtonState();
}
class _DraggableFloatingActionButtonState extends State<DraggableFloatingActionButton> {
bool _isDragging = false;
late Offset _offset;
@override
void initState() {
super.initState();
_offset = widget.initialOffset;
}
void _updatePosition(PointerMoveEvent pointerMoveEvent) {
double newOffsetX = _offset.dx pointerMoveEvent.delta.dx;
double newOffsetY = _offset.dy pointerMoveEvent.delta.dy;
setState(() {
_offset = Offset(newOffsetX, newOffsetY);
});
}
@override
Widget build(BuildContext context) {
return Positioned(
left: _offset.dx,
top: _offset.dy,
child: Listener(
onPointerMove: (PointerMoveEvent pointerMoveEvent) {
_updatePosition(pointerMoveEvent);
setState(() {
_isDragging = true;
});
},
onPointerUp: (PointerUpEvent pointerUpEvent) {
print('onPointerUp');
if (_isDragging) {
setState(() {
_isDragging = false;
});
} else {
widget.onPressed();
}
},
child: widget.child,
),
);
}
}
需要处理的另一件事是防止浮动操作按钮脱离父级框。如果我们忽略这一点,用户可以将按钮拖到父框之外。这意味着有必要知道父级的宽度和高度。您需要向父小部件添加一个键并将其传递给DraggableFloatingActionButton
小部件从key中,你可以从currentContext属性中获取RenderBox,它有findRenderObject方法。然后,您可以从 RenderBox 的 size 属性中获取父级的大小。您必须小心,因为必须在构建树之后调用 findRenderObject 方法。因此,您需要使用 WidgetsBinding 的 addPostFrameCallback 来调用它。
获得父尺寸后,您可以计算水平和垂直轴上的最小和最大偏移量。不仅是父尺寸,您还需要考虑按钮尺寸来确定最大偏移量。因此,您需要为子小部件做类似的事情。对于子部件,可以将其包装为 Container 的子部件并将 GlobalKey 传递给 Container。
_updatePosition 方法也需要调整。如果新偏移量低于最小偏移量,则必须将该值设置为最小偏移量。如果新偏移量大于最大偏移量,则必须将该值设置为最大偏移量。您需要对 x 轴和 y 轴执行此操作。
代码语言:javascript复制 class DraggableFloatingActionButton extends StatefulWidget {
final Widget child;
final Offset initialOffset;
final VoidCallback onPressed;
DraggableFloatingActionButton({
required this.child,
required this.initialOffset,
required this.onPressed,
});
@override
State<StatefulWidget> createState() => _DraggableFloatingActionButtonState();
}
class _DraggableFloatingActionButtonState extends State<DraggableFloatingActionButton> {
final GlobalKey _key = GlobalKey();
bool _isDragging = false;
late Offset _offset;
late Offset _minOffset;
late Offset _maxOffset;
@override
void initState() {
super.initState();
_offset = widget.initialOffset;
WidgetsBinding.instance?.addPostFrameCallback(_setBoundary);
}
void _setBoundary(_) {
final RenderBox parentRenderBox = widget.parentKey.currentContext?.findRenderObject() as RenderBox;
final RenderBox renderBox = _key.currentContext?.findRenderObject() as RenderBox;
try {
final Size parentSize = parentRenderBox.size;
final Size size = renderBox.size;
setState(() {
_minOffset = const Offset(0, 0);
_maxOffset = Offset(
parentSize.width - size.width,
parentSize.height - size.height
);
});
} catch (e) {
print('catch: $e');
}
}
void _updatePosition(PointerMoveEvent pointerMoveEvent) {
double newOffsetX = _offset.dx pointerMoveEvent.delta.dx;
double newOffsetY = _offset.dy pointerMoveEvent.delta.dy;
if (newOffsetX < _minOffset.dx) {
newOffsetX = _minOffset.dx;
} else if (newOffsetX > _maxOffset.dx) {
newOffsetX = _maxOffset.dx;
}
if (newOffsetY < _minOffset.dy) {
newOffsetY = _minOffset.dy;
} else if (newOffsetY > _maxOffset.dy) {
newOffsetY = _maxOffset.dy;
}
setState(() {
_offset = Offset(newOffsetX, newOffsetY);
});
}
@override
Widget build(BuildContext context) {
return Positioned(
left: _offset.dx,
top: _offset.dy,
child: Listener(
onPointerMove: (PointerMoveEvent pointerMoveEvent) {
_updatePosition(pointerMoveEvent);
setState(() {
_isDragging = true;
});
},
onPointerUp: (PointerUpEvent pointerUpEvent) {
print('onPointerUp');
if (_isDragging) {
setState(() {
_isDragging = false;
});
} else {
widget.onPressed();
}
},
child: Container(
key: _key,
child: widget.child,
),
),
);
}
}
完整代码
下面是使用上述DraggableFloatingActionButton
类的完整代码。一个简单的圆形小部件作为child
参数传递,这意味着它成为可拖动的按钮。您可以为按钮使用任何小部件,包括 Flutter 的FloatingActionButton
小部件。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '坚果前端',
home: DraggableFloatingActionButtonExample(),
);
}
}
class DraggableFloatingActionButtonExample extends StatelessWidget {
final GlobalKey _parentKey = GlobalKey();
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: const Text('公众号:坚果前端'),
centerTitle: true,
backgroundColor: Colors.teal,
),
body: Column(
children: [
Container(
height: 100,
),
Container(
width: 300,
height: 300,
child: Stack(
key: _parentKey,
children: [
Container(color: Colors.teal),
Center(
child: const Text(
'坚果前端',
style: const TextStyle(color: Colors.white, fontSize: 24),
),
),
DraggableFloatingActionButton(
child: Container(
width: 50,
height: 50,
decoration: ShapeDecoration(
shape: CircleBorder(),
color: Colors.white,
),
child:
Icon(Icons.flutter_dash, color: Colors.blue, size: 50),
),
initialOffset: const Offset(100, 100),
parentKey: _parentKey,
onPressed: () {
print('Button is clicked');
},
),
],
),
)
],
),
);
}
}
class DraggableFloatingActionButton extends StatefulWidget {
final Widget child;
final Offset initialOffset;
final VoidCallback onPressed;
final GlobalKey parentKey;
DraggableFloatingActionButton({
required this.child,
required this.initialOffset,
required this.onPressed,
required this.parentKey,
});
@override
State<StatefulWidget> createState() => _DraggableFloatingActionButtonState();
}
class _DraggableFloatingActionButtonState
extends State<DraggableFloatingActionButton> {
final GlobalKey _key = GlobalKey();
bool _isDragging = false;
late Offset _offset;
late Offset _minOffset;
late Offset _maxOffset;
@override
void initState() {
super.initState();
_offset = widget.initialOffset;
WidgetsBinding.instance?.addPostFrameCallback(_setBoundary);
}
void _setBoundary(_) {
final RenderBox parentRenderBox =
widget.parentKey.currentContext?.findRenderObject() as RenderBox;
final RenderBox renderBox =
_key.currentContext?.findRenderObject() as RenderBox;
try {
final Size parentSize = parentRenderBox.size;
final Size size = renderBox.size;
setState(() {
_minOffset = const Offset(0, 0);
_maxOffset = Offset(
parentSize.width - size.width, parentSize.height - size.height);
});
} catch (e) {
print('catch: $e');
}
}
void _updatePosition(PointerMoveEvent pointerMoveEvent) {
double newOffsetX = _offset.dx pointerMoveEvent.delta.dx;
double newOffsetY = _offset.dy pointerMoveEvent.delta.dy;
if (newOffsetX < _minOffset.dx) {
newOffsetX = _minOffset.dx;
} else if (newOffsetX > _maxOffset.dx) {
newOffsetX = _maxOffset.dx;
}
if (newOffsetY < _minOffset.dy) {
newOffsetY = _minOffset.dy;
} else if (newOffsetY > _maxOffset.dy) {
newOffsetY = _maxOffset.dy;
}
setState(() {
_offset = Offset(newOffsetX, newOffsetY);
});
}
@override
Widget build(BuildContext context) {
return Positioned(
left: _offset.dx,
top: _offset.dy,
child: Listener(
onPointerMove: (PointerMoveEvent pointerMoveEvent) {
_updatePosition(pointerMoveEvent);
setState(() {
_isDragging = true;
});
},
onPointerUp: (PointerUpEvent pointerUpEvent) {
print('onPointerUp');
if (_isDragging) {
setState(() {
_isDragging = false;
});
} else {
widget.onPressed();
}
},
child: Container(
key: _key,
child: widget.child,
),
),
);
}
}
输出:
概括
这就是如何在 Flutter 中创建可拖动的浮动操作按钮。基本上,您可以使用Listener
小部件来检测指针移动事件并根据移动增量更新按钮偏移。该Listener
小部件还支持检测应执行按钮操作的指针向上事件,除非它刚刚被拖动。您还需要获取父级和按钮的大小,以防止按钮脱离父级框。