news 2026/7/2 15:32:24

Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(三)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(三)

Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(三)

Flutter: 3.35.6

因为实现了单个的,给出github链接:https://github.com/yhtqw/FrontEndDemo

前面我们简单实现了元素的平移和缩放,接下来我们继续实现旋转功能。

元素的旋转会改变角度,角度一变,那么响应事件的热区也会跟着改变,所以我们得提前考虑这些会因为角度改变而改变的地方。

先来简单实现一下旋转,先不考虑上述的热区问题。

要实现旋转,我们就得知道元素的旋转角度,主要得出旋转的角度,那么实现起来就比较简单,所以简单使用数学的知识分析一下吧

从我们这个需求中可以提取到的数据为按下点的坐标,拖动时变换的坐标;所以我们能否根据一个点的坐标,计算出该点与某点形成的夹角,好像刚好有个满足部分,就是arctan2,arctan2的主要作用是根据一个点的坐标,计算出该点与坐标原点所形成的夹角(主要作用);如果我们要知道给出点与任意(x’, y’)形成的夹角呢?前面的arctan2中将坐标原点换成任意某点不就行了?

使用 arctan2 计算两点连线的角度,核心是计算两点之间的坐标差 (Δx, Δy),然后将其作为 arctan2 的参数。所以实现起来就比较简单了。其实这个实现的原理在很多地方都一样,例如web端的元素拖动旋转也可以使用这个原理。实现的方式应该不止一种吧,只要能计算出这个角度就行了。

值得注意的是,现在我们研究的是单个元素,所以坐标系就是以元素自身形成的(响应的事件也是在这个元素上),等后期要实现多个,坐标系就得以外层容器作为参考了。

// 其他省略.../// 新增旋转状态热区字符串constString statusRotate='rotate';/// 抽取响应旋转操作区域的大小finaldouble rotateWidth=20;finaldouble rotateHeight=20;/// 旋转角度double rotateNumber=0;double initRotateNumber=0;void_onPanUpdate(DragUpdateDetails details){print('更新: $details');if(status==statusMove){_onMove(details.localPosition.dx,details.localPosition.dy);}elseif(status==statusScale){_onScale(details.delta.dx,details.delta.dy);}elseif(status==statusRotate){// 新增旋转热区的响应事件_onRotate(details.localPosition.dx,details.localPosition.dy);}}void_onPanEnd(){print('抬起或者因为某些原因并没有触发onPanDown事件');setState((){// 当次结束后重新记录,也可以在按下时记录initX=x;initY=y;// 新增旋转角度的记录initRotateNumber=rotateNumber;});}/// 处理旋转void_onRotate(double dx,double dy){/// 要计算点 (x, y) 与任意点 (x', y') 连线所成的角度,可以使用 arctan2 函数。/// 关键在于将两点之间的相对坐标差作为 arctan2 的输入参数。/// 这里我们以元素的中心为旋转中心/// 利用上述方法计算起始点(按下时)与中心的连线组成的夹角为初始夹角,/// 拖动的点与中心点连线组层的夹角为结束时的夹角,/// 通过初始夹角与结束夹角计算旋转的角度// 确定旋转中心,因为这里的拖动是单个元素,坐标都是相对于元素自身形成的坐标系,所以坐标中心始终都是元素的中心double centerX=elementWidth/2;double centerY=elementHeight/2;double diffStartX=startPosition.dx-centerX;double diffStartY=startPosition.dy-centerY;double diffEndX=dx-centerX;double diffEndY=dy-centerY;double angleStart=atan2(diffStartY,diffStartX);double angleEnd=atan2(diffEndY,diffEndX);setState((){rotateNumber=initRotateNumber+angleEnd-angleStart;});}/// 判断点击在什么区域String?_onDownZone(double x,double y){if(x>=elementWidth-scaleWidth&&x<=elementWidth&&y>=elementHeight-scaleHeight&&y<=elementHeight){returnstatusScale;}elseif(x>=elementWidth-rotateHeight&&x<=elementWidth&&y>=0&&y<=rotateHeight){// 固定右上角为旋转热区returnstatusRotate;}elseif(x>=0&&x<=elementWidth&&y>=0&&y<=elementHeight){returnstatusMove;}returnnull;}// 新增响应旋转操作Positioned(left:x,top:y,child:Transform.rotate(angle:rotateNumber,child:GestureDetector(onPanDown:_onPanDown,onPanUpdate:_onPanUpdate,onPanEnd:(details)=>_onPanEnd(),onPanCancel:_onPanEnd,child:Container(width:elementWidth,height:elementHeight,color:Colors.transparent,child:Stack(alignment:Alignment.center,clipBehavior:Clip.none,children:[Container(width:elementWidth,height:elementHeight,color:Colors.amber,),// 响应旋转操作Positioned(top:0,right:0,child:Container(width:scaleWidth,height:scaleHeight,color:Colors.white,),),// 响应缩放操作],),),),),),// 其他省略...

运行效果:

这样就简单实现了旋转。然后我们继续考虑热区的问题,当旋转一定角度的时候,再次点击对应的热区,就无法响应事件了,因为旋转后热区坐标已经发生改变,所以我们得对点击判断中加入角度的影响。

已知某点坐标和旋转角度,求旋转后的坐标值?

要计算旋转后的坐标,可以使用旋转矩阵。给定一个点 (x, y) 绕原点逆时针旋转角度 θ 后的新坐标 (x’, y’) 计算公式如下:

x’ = x * cosθ - y * sinθ;
y’ = x * sinθ + y * cosθ;

如果我们是绕任意点而不是原点,需要先平移坐标系

  1. 平移: 将 (x, y) 平移到原点,新坐标为 (x - a, y - b);
  2. 旋转: 按照上述公式计算 (x’, y’);
  3. 平移回原坐标系: 新坐标为(x’ + a, y’ + b)。

基于上面的公式,我们更改热区点击判断方法:

/// 判断点击在什么区域String?_onDownZone(double x,double y){finaloffsetScale=rotatePoint(elementWidth,elementHeight);// 设置都是最大的顶点坐标,方便下面判断区域的方式结构一致// 后续就好抽取方法finaloffsetRotate=rotatePoint(elementWidth,rotateHeight);if(x>=offsetScale.dx-scaleWidth&&x<=offsetScale.dx&&y>=offsetScale.dy-scaleHeight&&y<=offsetScale.dy){returnstatusScale;}elseif(x>=offsetRotate.dx-rotateHeight&&x<=offsetRotate.dx&&y>=offsetRotate.dy-rotateHeight&&y<=offsetRotate.dy){returnstatusRotate;}elseif(x>=0&&x<=elementWidth&&y>=0&&y<=elementHeight){returnstatusMove;}returnnull;}/// 计算旋转后的点坐标OffsetrotatePoint(double x,double y){finaldeg=rotateNumber*pi/180;// 确定旋转中心,因为这里的拖动是单个元素,坐标都是相对于元素自身形成的坐标系,所以坐标中心始终都是元素的中心finalcenterX=elementWidth/2;finalcenterY=elementHeight/2;finaldiffX=x-centerX;finaldiffY=y-centerY;finaldx=diffX*cos(deg)-diffY*sin(deg)+centerX;finaldy=diffX*sin(deg)+diffY*cos(deg)+centerY;returnOffset(dx,dy);}

可以看到的是旋转和缩放热区即使在旋转后依然能够正常响应,还有最后一点,就是移动的时候也要应用旋转角度计算,因为我们使用的是元素自身为坐标系,坐标系旋转了,自然移动时的计算方式也得跟着变,其实对于后期将事件应用到容器上了过后就不需要考虑这些了,因为外层容器并不会变换,所以后期不使用逆运算,所以我们这里直接使用globalPosition来计算值即可(变换计算坐标感兴趣的可以自行研究一下):

void_onPanDown(DragDownDetails details){print('按下: $details');String?tempStatus=_onDownZone(details.localPosition.dx,details.localPosition.dy);print(tempStatus);setState((){if(tempStatus==statusMove){// 如果是移动,则使用globalPositionstartPosition=details.globalPosition;}else{startPosition=details.localPosition;}status=tempStatus;});}void_onPanUpdate(DragUpdateDetails details){print('更新: $details');if(status==statusMove){_onMove(details.globalPosition.dx,details.globalPosition.dy);}elseif(status==statusScale){_onScale(details.delta.dx,details.delta.dy);}elseif(status==statusRotate){_onRotate(details.localPosition.dx,details.localPosition.dy);}}

这样就对单个元素实现了变换的效果,前置就算时铺垫完成了,后续就开始实现多个的。

感兴趣的也可以关注我的微信公众号【前端学习小营地】,不定时会分享一些小功能~

今天的分享到此结束,感谢阅读~拜拜~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 1:10:16

LeetCode 3606.优惠券校验器:分类 + 排序

【LetMeFly】3606.优惠券校验器&#xff1a;分类 排序 力扣题目链接&#xff1a;https://leetcode.cn/problems/coupon-code-validator/ 给你三个长度为 n 的数组&#xff0c;分别描述 n 个优惠券的属性&#xff1a;code、businessLine 和 isActive。其中&#xff0c;第 i 个…

作者头像 李华
网站建设 2026/7/1 23:40:33

ensp vlan实验作业

一、实验拓扑&#xff08;截自己的拓扑图&#xff0c;附加上地址分配情况&#xff09;二、实验需求1、全网可达&#xff1b;2、使用DHCP获取IP地址&#xff1b;三、配置思路1、在各个交换机上创建vlan2、分析链路类型&#xff0c;配置相应的接口为access口或是trunk口3、配置路…

作者头像 李华
网站建设 2026/7/1 20:15:18

24、网页开发技术综合解析

网页开发技术综合解析 1. 基础概念与环境搭建 在网页开发领域,有众多基础概念和环境搭建的要点需要掌握。首先是互联网相关的概念,互联网地址、IP 协议以及互联网服务提供商(ISP)是网络连接的基础。而在操作系统方面,Linux 是一个重要的选择。Linux 有多种发行版,如 Re…

作者头像 李华
网站建设 2026/7/2 0:20:57

39、SQL Server管理与监控全解析

SQL Server管理与监控全解析 1. 服务器端代码管理 在SQL Server中,创建T - SQL服务器端代码,如存储过程、视图、函数和触发器后,可能需要对其进行修改或删除。每种对象类型都支持与CREATE语法对应的ALTER和DROP T - SQL版本。 当使用ALTER修改服务器端编程对象(如存储过…

作者头像 李华
网站建设 2026/7/2 0:33:23

计算机毕业设计必看必学~ 基于SSM的大学生就业平台的设计与实现85751,原创定制程序、单片机、java、PHP、Python、小程序、文案全套、毕设成品等!

目录 1 绪论 1.1 选题背景与意义 1.2国内外研究现状 1.3论文结构与章节安排 2系统分析 2.1 可行性分析 2.1.1技术可行性 2.1.2经济可行性 2.1.3社会可行性 2.2 系统流程分析 2.2.1系统开发流程 2.2.2 用户登录流程 2.2.3 系统操作流程 2.2.4 添加信息流程 2.2.5 …

作者头像 李华