Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(一)
Flutter: 3.35.6
之前使用 html + javascript 实现了类似的功能,部分地方可以借鉴一下,现在基于此在flutter端实现一个容器内部元素可拖动,缩放,旋转的功能。
这个功能主要用于某个场景的装饰(例如家园,有各种家具,用户可以操作它们的摆放位置),组合图片(将多个图片在容器内随意摆放,组合成一张图)等等。所以还是比较实用的一个小功能。
依然是一个一个功能分析实现,首先来看平移。
一般来说,我们要移动这个元素,响应这个操作的区域大概率是在这个元素的内部,而且需要移动,所以我们就知道了响应移动操作是按下这个元素的内部区域并移动(元素内部按下移动响应移动操作)。为了响应这个操作,查阅文档,可以知道 GestureDetector 就可以满足,接下来初步体验一下:
dart
import 'package:flutter/material.dart';
class TestGestureDetector extends StatelessWidget {
const TestGestureDetector({super.key});
void _onPanDown(DragDownDetails details) {
print('按下: $details');
}
void _onPanUpdate(DragUpdateDetails details) {
print('更新: $details');
}
void _onPanEnd(DragEndDetails details) {
print('抬起: $details');
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: Container(
width: 100,
height: 100,
color: Colors.amber,
),
);
}
}
运行效果:

可以看到手势的监听满足我们的需求,基于此开始实现一个元素的拖动。
限制在一个范围内拖动元素,使用 Stack + Positioned 来实现这样的元素,通过拖动改变 Positioned 的 top 值和 left 值:
dart
import 'package:flutter/material.dart';
class DragContainer extends StatefulWidget {
const DragContainer({super.key});
@override
State<DragContainer> createState() => _DragContainerState();
}
class _DragContainerState extends State<DragContainer> {
double x = 10;
double y = 10;
void _onPanDown(DragDownDetails details) {
print('按下: $details');
}
void _onPanUpdate(DragUpdateDetails details) {
print('更新: $details');
// delta 表示自上次更新以来的位置偏移量
setState(() {
x += details.delta.dx;
y += details.delta.dy;
});
}
void _onPanEnd(DragEndDetails details) {
print('抬起: $details');
}
@override
Widget build(BuildContext context) {
return Container(
width: 300,
height: 600,
color: Colors.blueAccent,
child: Stack(
children: [
Positioned(
left: x,
top: y,
child: GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: Container(
width: 100,
height: 100,
color: Colors.amber,
),
),
),
],
),
);
}
}
运行效果:

就这样我们实现了一个简单的元素可拖动效果,从上面我们不难发现,这个元素也可以拖到容器外面,如果元素完全拖动到容器外,那我们就完全无法看到并操作这个元素了,所以意义不大,所以我们限制只允许在容器内拖动。我们接着更改代码:
dart
import 'package:flutter/material.dart';
class DragContainer extends StatefulWidget {
const DragContainer({super.key});
@override
State<DragContainer> createState() => _DragContainerState();
}
class _DragContainerState extends State<DragContainer> {
/// 抽取容器的宽
final double containerWidth = 300;
/// 抽取容器的高
final double containerHeight = 600;
/// 抽取元素的宽
double elementWidth = 100;
/// 抽取元素的高
double elementHeight = 100;
double x = 10;
double y = 10;
void _onPanDown(DragDownDetails details) {
print('按下: $details');
}
void _onPanUpdate(DragUpdateDetails details) {
print('更新: $details');
setState(() {
x += details.delta.dx;
y += details.delta.dy;
// 限制左边界
if (x < 0) {
x = 0;
}
// 限制右边界
if (x > containerWidth - elementWidth) {
x = containerWidth - elementHeight;
}
// 限制上边界
if (y < 0) {
y = 0;
}
// 限制下边界
if (y > containerHeight - elementWidth) {
y = containerHeight - elementHeight;
}
});
}
void _onPanEnd(DragEndDetails details) {
print('抬起: $details');
}
@override
Widget build(BuildContext context) {
return Container(
width: containerWidth,
height: containerHeight,
color: Colors.blueAccent,
child: Stack(
children: [
Positioned(
left: x,
top: y,
child: GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: Container(
width: elementWidth,
height: elementHeight,
color: Colors.amber,
),
),
),
],
),
);
}
}
运行效果:

现在我们对边界做出了限制,但新的问题又出现了,我们手指拖动到容器外面,依然在响应事件,此时移动到容器内的过程中,元素也在跟着移动,待完全将手指移动到容器内,元素和手指已经隔了很长一个距离了。
这是因为 delta 中记录着自上次更新的偏移量,即便是手指在容器外,依然会记录这个值,从而实时的更改这个值,当我们手指在某一个方法超出范围时,继续向那个范围外移动,元素因为限制所以不会超出容器外,指针和元素越离越远,当我们向回走时,y += details.delta.dy; (以下边界为例),dy 为负值,y + dy 就小于了边界值,所以就直接同步移动,这就导致了指针和元素有了一定的距离。
为了解决这个问题,我们可以使用下面这种方法,将手指按下,移动,抬起看做一个操作动作,用变量记录当前的初始的x和y,手指按下记录按下的坐标,手指移动时记录移动到的坐标,通过按下的坐标与移动的坐标计算移动的距离,那么就有:当前的值 = 初始化值 + (手指当前移动的值 - 手指按下的值);在当次操作动作过程中,我们始终以当前初始的x和y为标准,以按下的位置做和移动的位置来计算,这样即便是移动到容器外再往回移动的过程中,使用初始的x和y、初始按下位置和移动的位置(只要移动的位置还是在边界外)计算出来依然大于边界值。这样就达到了我们的要求。那么新的问题来了,我们怎么拿到按下的坐标和移动的坐标呢?我们看看打印的值有些什么:

可以看到还有以下的值供我们使用:
- globalPosition: 提供指针在全局坐标系中的当前位置坐标
- localPosition: 表示指针相对于当前监听组件坐标系的位置坐标
所以我们只需要计算出当前位置即可,这里使用 localPosition 来计算:
dart
import 'package:flutter/material.dart';
class DragContainer extends StatefulWidget {
const DragContainer({super.key});
@override
State<DragContainer> createState() => _DragContainerState();
}
class _DragContainerState extends State<DragContainer> {
/// 抽取容器的宽
final double containerWidth = 300;
/// 抽取容器的高
final double containerHeight = 600;
/// 抽取元素的宽
double elementWidth = 100;
/// 抽取元素的高
double elementHeight = 100;
double x = 10;
double y = 10;
double initX = 10;
double initY = 10;
Offset startPosition = Offset(0, 0);
void _onPanDown(DragDownDetails details) {
print('按下: $details');
setState(() {
startPosition = details.localPosition;
});
}
void _onPanUpdate(DragUpdateDetails details) {
print('更新: $details');
setState(() {
// 计算方法
x = initX + details.localPosition.dx - startPosition.dx;
y = initY + details.localPosition.dy - startPosition.dy;
// 限制左边界
if (x < 0) {
x = 0;
}
// 限制右边界
if (x > containerWidth - elementWidth) {
x = containerWidth - elementWidth;
}
// 限制上边界
if (y < 0) {
y = 0;
}
// 限制下边界
if (y > containerHeight - elementHeight) {
y = containerHeight - elementHeight;
}
});
}
void _onPanEnd() {
print('抬起或者因为某些原因并没有触发onPanDown事件');
setState(() {
// 当次结束后重新记录,也可以在按下时记录
initX = x;
initY = y;
});
}
@override
Widget build(BuildContext context) {
return Container(
width: containerWidth,
height: containerHeight,
color: Colors.blueAccent,
child: Stack(
children: [
Positioned(
left: x,
top: y,
child: GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: (details) => _onPanEnd,
onPanCancel: () => _onPanEnd,
child: Container(
width: elementWidth,
height: elementHeight,
color: Colors.amber,
),
),
),
],
),
);
}
}
运行效果:

可以看到超出边界往回走的时候指针计算的位置依然大于边界值,直到回到按下的地方相对位置(按下时相对于黄色区域的位置坐标)才重新移动。
这样我们就初步实现了元素的移动,这又是一个系列的文章,后面再慢慢实现其他的功能。
感兴趣的也可以关注我的微信公众号【前端学习小营地】,不定时会分享一些小功能~
至此该篇结束,感谢阅读~拜拜~