Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(二)
Flutter: 3.35.6
前面我们简单实现了元素的平移,接下来我们继续实现缩放功能。
缩放的功能大概率是在元素的某个区域内响应事件,前期我们以快速实现功能为主,所以暂定右下角为响应缩放操作的热区。因为现在我们还是研究一个元素的操作,所以事件都设置在这个元素上。既然定了右下角为响应缩放的区域,那么元素内的其他区域则响应移动,所以就得区分按下的区域在哪里,通过点击所在区域来判断响应的事件,基于前面的代码更改我们需要新增按下区域的判断逻辑,抽取移动事件应该响应操作的方法,所以做出如下的更改:
import 'package:flutter/material.dart';
/// 抽取状态字符串,用于判断点击的区域
const String statusMove = 'move';
const String statusScale = 'scale';
class TransformContainer extends StatefulWidget {
const TransformContainer({super.key});
@override
State createState() => _TransformContainerState();
}
class _TransformContainerState extends State {
/// 抽取容器的宽
final double containerWidth = 300;
/// 抽取容器的高
final double containerHeight = 600;
/// 抽取响应缩放操作区域的大小
final double scaleWidth = 20;
final double scaleHeight = 20;
/// 抽取元素的宽
double elementWidth = 100;
/// 抽取元素的高
double elementHeight = 100;
double x = 10;
double y = 10;
double initX = 10;
double initY = 10;
Offset startPosition = Offset(0, 0);
/// 响应操作的状态字符串
String? status;
void _onPanDown(DragDownDetails details) {
print('按下: $details');
// 通过点击的区域判断当前元素应该处以什么操作状态
String? tempStatus = _onDownZone(
details.localPosition.dx,
details.localPosition.dy,
);
print(tempStatus);
setState(() {
startPosition = details.localPosition;
status = tempStatus;
});
}
void _onPanUpdate(DragUpdateDetails details) {
print('更新: $details');
if (status == statusMove) {
// 如果在移动区域,则响应移动事件
_onMove(details.localPosition.dx, details.localPosition.dy);
} else if (status == statusScale) {
// 如果在缩放区域,则响应缩放事件
_onScale();
}
}
void _onPanEnd() {
print('抬起或者因为某些原因并没有触发onPanDown事件');
setState(() {
// 当次结束后重新记录,也可以在按下时记录
initX = x;
initY = y;
});
}
/// 抽取处理移动的方法
void _onMove(double dx, double dy) {
setState(() {
// 计算方法
x = initX + dx - startPosition.dx;
y = initY + 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 _onScale() {
// 实现缩放逻辑
}
/// 判断点击在什么区域
String? _onDownZone(double x, double y) {
if (
x >= elementWidth - scaleWidth &&
x <= elementWidth &&
y >= elementHeight - scaleHeight &&
y <= elementHeight
) {
// 右下角响应缩放操作
return statusScale;
} else if (
x >= 0 &&
x <= elementWidth &&
y >= 0 &&
y <= elementHeight
) {
// 在元素内部则响应移动操作
return statusMove;
}
return null;
}
@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: SizedBox(
width: elementWidth,
height: elementHeight,
child: Stack(
alignment: Alignment.center,
clipBehavior: Clip.none,
children: [
Container(
width: elementWidth,
height: elementHeight,
color: Colors.amber,
),
// 响应缩放操作
Positioned(
bottom: 0,
right: 0,
child: Container(
width: scaleWidth,
height: scaleHeight,
color: Colors.white,
),
),
],
),
),
),
),
],
),
);
}
}
运行效果:
可以看到,我们抽取了响应的区域,当点击对应的区域则可以实现响应的操作。接下来我们实现缩放的逻辑。
通过按下移动的过程中,我们可以得到下面这些数据:
- globalPosition: 提供指针在全局坐标系中的当前位置坐标
- localPosition: 表示指针相对于当前监听组件坐标系的位置坐标
- delta: 表示自上次更新以来的位置偏移量
缩放主要是对这个元素的宽高进行,使用delta中的数据改变宽高即可:
// 其他省略...
void _onPanUpdate(DragUpdateDetails details) {
print('更新: $details');
if (status == statusMove) {
_onMove(details.localPosition.dx, details.localPosition.dy);
} else if (status == statusScale) {
_onScale(details.delta.dx, details.delta.dy);
}
}
/// 处理缩放
void _onScale(double dx, double dy) {
// 宽高加上每次移动的偏移量实现缩放
setState(() {
elementHeight += dy;
elementWidth += dx;
});
}
// 其他省略...
运行效果:
这样就简单的实现了缩放,如果没有其他的要求,不在意拉伸挤压的话,这就完成了,实际上肯定有需要保持等比例缩放(例如对一张图片的缩放,如果拉伸挤压后就很难看了)。
实现等比例拉伸,我们需要以某个标准,例如以宽还是以高,如果以宽为标准,则宽度变化时,高度通过初始宽度和现在宽度的比例计算得出,反之亦然(或者以变化过程中变化较快的一边为标准):
/// 新增是否使用等比例
bool useProportional = true;
/// 处理缩放
void _onScale(double dx, double dy) {
if (useProportional) {
// 以宽为基准等比计算高度
double tempWidth = elementWidth + dx;
setState(() {
elementHeight = elementHeight * (tempWidth / elementWidth);
elementWidth = tempWidth;
});
} else {
setState(() {
elementHeight += dy;
elementWidth += dx;
});
}
}
运行效果:
可以看到,现在就简单实现了等比缩放了,目前这样依然有一个问题,如果我们缩小到负数,元素的宽高就变成了负值,这明显会报错,所以我们得对极限情况做出修正:
/// 新增最小值
final double minWidth = 40;
final double minHeight = 40;
/// 处理缩放
void _onScale(double dx, double dy) {
double tempWidth = elementWidth + dx;
// 单独演示使用宽度为基准,后面可以更改为以短边为基准,处理的逻辑差不多
// 限制边界值,使用clamp和下面注释的逻辑是一样的
tempWidth = tempWidth.clamp(minWidth, containerWidth);
// if (tempWidth < minWidth) {
// tempWidth = minWidth;
// } else if (tempWidth > containerWidth) {
// tempWidth = containerWidth;
// }
if (useProportional) {
setState(() {
elementHeight = elementHeight * (tempWidth / elementWidth);
elementWidth = tempWidth;
});
} else {
setState(() {
elementHeight += dy;
elementWidth = tempWidth;
});
}
}
运行效果:
这样我们又对边界情况做出了限制。接下来我们实现以元素中心为缩放中心的效果,如果我们想以元素的中心为缩放的中心(现在的中心是元素的左上角),这样在缩放过程中,对应的坐标值也得更着变化,缩放从原来的一边转换为双边。
之前的缩放以左上角为缩放中心,现在需要以元素中心为准,则在变化过程中加上双倍的变化值(中心向两边扩张或者缩小),再将坐标值变化对应的移动值即可(左上角坐标因为中心的变换需要跟着变化):
/// 处理缩放
void _onScale(double dx, double dy) {
// 加上双倍的变换值
double tempWidth = elementWidth + dx * 2;
// 限制边界值
tempWidth = tempWidth.clamp(minWidth, containerWidth);
if (useProportional) {
double tempHeight = elementHeight * (tempWidth / elementWidth);
setState(() {
x -= (tempWidth - elementWidth) / 2;
y -= (tempHeight - elementHeight) / 2;
elementHeight = tempHeight;
elementWidth = tempWidth;
});
} else {
// 非等比缩放直接应用变化
setState(() {
elementHeight += dy * 2;
elementWidth = tempWidth;
x -= dx;
y -= dy;
});
}
}
运行效果:
这样就简单实现了元素的缩放,因为只是快速演示,所以考虑的情况没有那么复杂,部分极限的情况也没有做限制。
感兴趣的也可以关注我的微信公众号【前端学习小营地】,不定时会分享一些小功能~
实现缩放的功能就到此结束,感谢阅读,拜拜~