前言
好久没有写文章了,最近在学习手势绘制时开发了一个简易的电子白板demo,后续会单独写一个电子白板的项目开发实战,在开发当中处理白板中各种手势时,增加矩阵的效果,但是在使用矩阵时碰到了发生矩阵变换后,需要重新计算当前相对的点位从而不影响当前手势的正确绘制,所以在我踩坑后,特地写下这篇文章,希望对你有帮助
效果视频
逻辑
这里不做详细的绘制逻辑,单提供绘制以及矩阵相对点位思想和示例代码
画布
class CustomWhiteBoard extends CustomPainter {
WhiteBoardHandle whiteBoardHandle = WhiteBoardHandle();
CustomWhiteBoard(this.whiteBoardHandle) : super(repaint: whiteBoardHandle);
@override
void paint(Canvas canvas, Size size) {
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.saveLayer(rect, Paint());
onMatrix4(canvas, size);
//...绘制
canvas.restore();
// TODO: implement paint
}
@override
bool shouldRepaint(covariant CustomWhiteBoard oldDelegate) {
return true;
}
// ...这里是绘制矩阵
void onMatrix4(Canvas canvas,Size size){
final Matrix4 result = Matrix4.identity();
result.multiply(whiteBoardHandle.matrix4.value);
canvas.transform(result.storage);
}
}
画板这里就是一个很简单的绘制矩阵而已,不过多讲解具体画板绘制
功能实现
我当前使用手势为:ScaleGestureRecognizer (缩放手势识别器) 非使用 PanGestureRecognizer
ScaleGestureRecognizer 和PanGestureRecognizer 手势不能一起使用,但是ScaleGestureRecognizer 中包含的功能已经足够使用了,不需要PanGestureRecognizer
这里是手势工厂基础代码
Map<Type, GestureRecognizerFactory> getGesture(){ var scaleGesture = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(() => ScaleGestureRecognizer(), (instance) { instance.onStart = onScaleStart; instance.onUpdate = onScaleUpdate; instance.onEnd = onScaleEnd; }); return { ScaleGestureRecognizer: scaleGesture }; }
绑定在视图的RawGestureDetector的gesture参数中即可
我们基础的手势和绘制逻辑好了,接下来着重一下手势的相关实现代码Start -> End
定义矩阵
在你的State中定义两个基础矩阵 Matrix4 matrix 和 Matrix4 tempMatrix
matrix用于全局矩阵,tempMatrix用于临时矩阵,用于保留矩阵状态
手势实现逻辑
我们先来思考一下矩阵在每个手势状态需要负责的逻辑
- Start(开始状态) 这里主要负责当前手势中的触点,一般用于平移的起始点缓存
- Update(修改状态) 这里主要负责矩阵三个核心使用点(平移、缩放、旋转)的相关逻辑处理
- End(结束状态) 这里主要负责tempMatrix(缓存矩阵)的缓存
既然基础思路有了,那我们具体看看实现代码
Start
void onScaleStart(ScaleStartDetails details){
//校验手势触点
if(details.pointerCount == 1){
//缓存手势起始点
state.handle.translateStartOffset = details.focalPoint;
}
}
Update
void onScaleUpdate(ScaleUpdateDetails details){
//当前画笔状态 这里是个枚举用来区分矩阵处理
switch(state.paintType.value){
case PaintType.scale:
state.handle.changeMatrix4(state.handle.tempMatrix.multiplied(Matrix4.diagonal3Values(scale,scale,1)));
break;
case PaintType.rotate:
state.handle.changeMatrix4(state.handle.recordMatrix.multiplied(Matrix4.rotationZ(details.rotation)));
break;
case PaintType.translate:
state.handle.changeMatrix4(Matrix4.translationValues(
details.focalPoint.dx - state.handle.scaleOffset.dx,
details.focalPoint.dy - state.handle.scaleOffset.dy,
1
).multiplied(state.handle.recordMatrix4));
break;
case PaintType.paint || PaintType.setting || PaintType.clear:
//绘制逻辑
break;
}
}
这里主要提要两点
①、代码中的changeMatrix4(Matrix4 newMatrix4) 核心逻辑就是更新matrix矩阵的状态而已
②、multiplied(Matrix4 newMatrix4)方法 为当前矩阵matrix4乘法(复制)新矩阵到当前的矩阵中
diagonal3Values(矩阵缩放 x ,y,z) 和 rotationZ(旋转z) 和 translationValues(平移 x,y,z)
end
void onScaleEnd(ScaleEndDetails details){
//这里没什么好讲的,就是tempMatrix = matrix ,手势停止后缓存当前手势以保留矩阵状态
state.handle.saveRecordMatrix4();
}
补充
你或许也会有疑问,当画布应用了矩阵变化之后,整个画布会被矩阵影响,从而导致当前手势的点位信息会有偏差,而这种情况下怎么处理呢?
我们需要 获取矩阵偏移量后的相对点位 ,在获取相对点位之前,需要考虑到平移和旋转的逻辑计算,经过我的测试,先计算旋转后的偏移量再计算平移的偏移量会导致点位偏差,缩放而不影响。
所以正确点位计算思想为:先计算平移后的偏移量之后再获取旋转的偏移量,这样拿到的点位则是正确的
旋转值的获取和处理
可能你会问,ScaleUpdateDetails类的参数下不是有rotation获取旋转值吗?
但是它的旋转值偏移量是基于你手势中心点得到旋转值,这个值对于我们相对点位计算来说是有误的,我们应该获取矩阵自身的旋转值 这个值Matrix4中没有直接提供,但是提供了 Matrix3 Matrix4.getRotation()的方法,我们需要从这个方法中重新获取可用的矩阵角度值
获取Matrix3的旋转角度方法
// 获取矩阵的旋转角度(以度数为单位)
double getRotationAngle(vector.Matrix3 matrix) {
// 提取旋转部分
double sinTheta = matrix.entry(1, 0); // 获取旋转矩阵的 sin(θ) 值
double cosTheta = matrix.entry(0, 0); // 获取旋转矩阵的 cos(θ) 值
// 计算旋转角度(弧度)
double rotationAngle = math.atan2(sinTheta, cosTheta);
// 将弧度转换为度数
double rotationDegrees = rotationAngle * (180 / math.pi);
return rotationDegrees;
}
除此之外,我们还需要根据点位信息和角度,转化为相对点位
首先先将当前矩阵的角度转化为弧度
// 将角度转换为弧度
var radians = angle * (math.pi / 180.0);
我们利用旋转矩阵公式
rotatedX=offset.dx×cos(angle)−offset.dy×sin(angle)
rotatedY=offset.dx×sin(angle)+offset.dy×cos(angle)
获取到相对矩阵偏移量
Offset _rotateOffset(Offset offset, double angle) {
// 将角度转换为弧度
var radians = angle * (math.pi / 180.0);
var rotatedX = offset.dx * math.cos(radians) - offset.dy * math.sin(radians);
var rotatedY = offset.dx * math.sin(radians) + offset.dy * math.cos(radians);
return Offset(rotatedX, rotatedY);
}
实现获取相对点位
直接贴代码吧,一看就明白
getRealOffset(Offset offset){
var matrix = matrix4.value;
// 获取矩阵平移的x,y
var translationX = matrix.getTranslation().x;
var translationY = matrix.getTranslation().y;
// 获取当前矩阵的缩放值
double scale = matrix.getMaxScaleOnAxis();
var matrix3 = matrix.getRotation();
double rotate = getRotationAngle(matrix3);
// 这里的Offset为,当前手势点位的Offset - 平移后的Offset
var rotatedOffset = _rotateOffset(offset - Offset(translationX, translationY), -rotate);
// 将偏移量向量旋转回原始坐标系
print('平移参数: (${translationX},${translationY}),缩放参数: ${scale},旋转角度: ${rotate},旋转偏移量: ${rotatedOffset}');
return rotatedOffset / scale;
}
至此结束。