梅州站改造高铁站,医疗网站建设讯息,网站建设的人才怎么称呼,企业公司网页设计方案Flutter 实现一个容器内部元素可平移、缩放和旋转等功能#xff08;六#xff09;
Flutter: 3.35.6
前面有人提到在元素内部的那块判断怎么那么写的#xff0c;看来对知识渴望的小伙伴还是有#xff0c;这样挺好的。不至于说牢记部分知识#xff0c;只需要大致了解一下有…Flutter 实现一个容器内部元素可平移、缩放和旋转等功能六Flutter: 3.35.6前面有人提到在元素内部的那块判断怎么那么写的看来对知识渴望的小伙伴还是有这样挺好的。不至于说牢记部分知识只需要大致了解一下有个印象后面如果哪里再用到了就可以根据这个印象去查阅资料。接下来我们说一下判断原理。当我们知晓矩形的四个顶点坐标(包括任意旋转后)可以使用向量叉乘法来判断是否在矩形内部。向量叉乘法的核心思想就是如果一个点在凸多边形内部那么它应该始终位于该多边形每条边的同一侧。注凸多边形定义为所有内角均小于180度并且任意两点之间的连线都完全位于多边形内部或边界。所以我们利用向量叉乘法来判断点位于线的哪一侧。假设矩形顶点按顺时针或逆时针顺序为 ABCD对于边 AB计算向量 AB 和 AP 的叉积。对于边 BC计算向量 BC 和 BP 的叉积。对于边 CD计算向量 CD 和 CP 的叉积。对于边 DA计算向量 DA 和 DP 的叉积。如果点 P 在所有边的同一侧即所有叉积结果的符号相同那么点 P 就在矩形内部。我们假设矩形某条边的顶点为(x1, y1), (x2, y2), 判断的点坐标为(x, y)那么就有(x2 - x1) * (y - y1) - (y2 - y1) * (x - x1) 0这样就可以判断在某侧如果其他三条边也满足那就是内侧了要转换为下面代码中的形式那就做一下加减乘除就行了(x2 - x1) * (y - y1) - (y2 - y1) * (x - x1) 0: 初始(x2 - x1) * (y - y1) / (y2 - y1) - (x - x1) 0: 两边同时除以(y2 - y1)(x2 - x1) * (y - y1) / (y2 - y1) - x x1 0: 展开括号(x2 - x1) * (y - y1) / (y2 - y1) x1 x: 将x移项这样就得到了代码中的判断依据至于循环遍历顶点的写法就是为了获取相邻两个顶点这个就可以带入square坐标和循环去算一下就行了保证每次循环都是相邻的两个顶点。我们使用的顶点坐标顺序是顺时针第一次循环 i 0j 3那么i就是左上顶点j就是左下顶点两个顶点刚好构成矩形左边第二次循环 j i此时 j 0i 1后续喜欢以此类推即可。这样判断在内侧差不多就解释完了。接下来开始我们今天正文。前面我们就简单完成了多个元素的相应操作剩下的就是一些优化和一些简单的扩展功能。既然是多个元素那么肯定就涉及到新增和删除之前的新增都是在列表里面直接添加现在我们单独提取一个方法用于新增。至于删除功能我们就定义在元素左上角为删除区域触发方式为点击。之前我们对热区的数据模型中添加了 trigger 字段用于表示当前区域触发操作的方式是什么所以我们得对点击方法进行优化并且在临时中间变量上面存储 trigger 字段用于判断classResponseAreaModel{// 其他省略.../// 当前响应操作的触发方式finalTriggerMethod trigger;}/// 新增返回Records用于记录状态和触发方式(ElementStatus,TriggerMethod)?_onDownZone({required double x,required double y,required ElementModel item,}){// 先判断是否在响应对应操作的区域final(ElementStatus,TriggerMethod)?areaStatus_getElementZone(x:x,y:y,item:item);if(areaStatus!null){returnareaStatus;}elseif(_insideElement(x:x,y:y,item:item)){// 因为加入旋转所以单独抽取落点是否在元素内部的方法return(ElementStatus.move,TriggerMethod.move);}returnnull;}/// 新增返回Records用于记录状态和触发方式(ElementStatus,TriggerMethod)?_getElementZone({required double x,required double y,required ElementModel item,}){// 新增Records记录返回的状态和触发方式(ElementStatus,TriggerMethod)?tempStatus;for(vari0;iConstantsConfig.baseAreaList.length;i){// 其他省略...if(xdx-areaCWxdxareaCWydy-areaCHydyareaCH){tempStatus(currentArea.status,currentArea.trigger);break;}}returntempStatus;}这样触发方式和状态都记录了我们就开始实现删除功能依然按照之前的步骤快速实现// 新增删除ResponseAreaModel(areaWidth:20,areaHeight:20,xRatio:0,yRatio:0,status:ElementStatus.deleteStatus,icon:assets/images/icon_delete.png,trigger:TriggerMethod.down,),/// 处理删除元素void_onDelete(){if(_currentElementnull)return;_elementList.removeWhere((item)item.id_currentElement?.id);}/// 按下事件void_onPanDown(DragDownDetails details){// 其他省略...// 遍历判断当前点击的位置是否落在了某个元素的响应区域for(varitemin_elementList){// 新增Records数据存储元素状态和触发方式final(ElementStatus,TriggerMethod)?status_onDownZone(x:dx,y:dy,item:item);if(status!null){currentElementitem;temptemp.copyWith(status:status.$1,trigger:status.$2);break;}}// 新增判断// 如果当前有选中的元素且和点击区域的currentElement是一个元素// 并且 temp 的 status对应的触发方式为点击那么就响应对应的点击事件if(currentElement?.id_currentElement?.idtemp.triggerTriggerMethod.down){if(temp.statusElementStatus.deleteStatus){_onDelete();// 因为是删除就置空选中让下面代码执行最后的清除currentElementnull;}}// 其他省略...}运行效果这样就简单实现了元素的删除功能。到此操作区域常用的功能差不多就完成接下来我们考虑一些区域的自定义例如我希望旋转的区域在右下角现在在右上角并且不使用缩放功能还想自定义一个区域这时候该如何实现呢允许传递配置通过这份配置来决定元素应该有什么响应区域并且是否使用这些响应区域然而操作这些内置的我们可以使用之前定义的final ElementStatus status;字段来确定要修改哪个区域毕竟一个操作应该是对应一个区域对于自定义区域我们的ElementStatus是个枚举类型且为必传这就限制了自定义区域所以我们的改造一下用户传递的自定义区域status为自行设置的字符串我们内部也同时更改为字符串涉及更改的地方有一些这里不做过多的说明后续可以查阅源码/// 元素当前操作状态/// 更改新增字符串的value属性enumElementStatus{move(value:move),rotate(value:rotate),scale(value:scale),deleteStatus(value:deleteStatus),;finalString value;constElementStatus({requiredthis.value});}/// 大致说一些需要更改的地方/// TemporaryModel 的 status 更改为字符串类型/// ResponseAreaModel 的 status 更改为字符串类型/// _onDownZone 方法中 Records 第一项也返回 String/// _getElementZone 方法中 Records 第一项也返回 String接下来我们确定自定义区域配置中需要的字段status用于映射内置的区域方便做更改String 必须use用于确定该 status 对应的内置区域是否使用bool 非必须xRatio用于确定区域位置double 非必须如果非内置的默认就是0yRatio用于确定区域位置double 非必须如果非内置的默认就是0trigger用于确定区域的触发方式TriggerMethod 非必须默认TriggerMethod.downicon用于确定操作区域的展示iconString 如果是内置的 status 就是非必须如果不是内置的就是必须fn自定义区域需要执行的方法Function({required double x, required double y}) 如果是内置的就是非必须如果不是内置的就必须基于上述开始进行编码/// 新增自定义区域配置classCustomAreaConfig{constCustomAreaConfig({requiredthis.status,this.use,this.xRatio,this.yRatio,this.triggerTriggerMethod.down,this.icon,this.fn,});/// 区域的操作状态字符串可以是内置的如果是内置的就覆盖内置的属性finalString status;/// 是否启用finalbool?use;/// 自定义位置finaldouble?xRatio;finaldouble?yRatio;/// 区域响应操作的触发方式finalTriggerMethod trigger;/// 自定义区域就是必传finalString?icon;/// 自定义区域就是必传点击对应的响应区域就执行自定义的方法finalFunction({required double x,required double y})?fn;}/// 新增自定义区域配置finalListCustomAreaConfig_customAreaList[// 不使用缩放区域CustomAreaConfig(status:ElementStatus.scale.value,use:false,),// 将旋转移到右下角CustomAreaConfig(status:ElementStatus.rotate.value,xRatio:1,yRatio:1,),];/// 容器响应操作区域之前是直接使用的常量里面的配置ListResponseAreaModel_areaList[];/// 初始化响应区域void_initArea(){ListResponseAreaModelareaList[];for(varareainConstantsConfig.baseAreaList){finalint index_customAreaList.indexWhere((item)item.statusarea.status);if(index-1){finalCustomAreaConfig customArea_customAreaList[index];// 如果是不使用则跳出本次循环if(customArea.usefalse){continue;}areaList.add(area.copyWith(xRatio:customArea.xRatio,yRatio:customArea.yRatio,icon:customArea.icon,fn:customArea.fn,));}else{areaList.add(area);}}setState((){_areaListareaList;});}// 其他省略.../// 抽取渲染的元素classTransformItemextendsStatelessWidget{constTransformItem({// 其他省略...requiredthis.areaList,});// 其他省略...finalListResponseAreaModelareaList;overrideWidgetbuild(BuildContext context){returnPositioned(left:elementItem.x,top:elementItem.y,// 新增旋转功能child:Transform.rotate(angle:elementItem.rotationAngle,child:Container(// 其他省略...// 新增区域的渲染child:selected?Stack(clipBehavior:Clip.none,children:[// 修改从外界传递区域列表...areaList.map((item)Positioned(// 其他省略...)),],):null,),));}}其他编码不算核心就不再展示了反正就一个之前从 ConstantsConfig.baseAreaList 拿的数据现在都直接使用 _areaList。运行效果可以看到我们将旋转区域移到右下角了并且不使用缩放区域这样就简单完成了区域自定义的配置。感兴趣的也可以关注我的微信公众号【前端学习小营地】不定时会分享一些小功能今天的分享就到此结束了感谢阅读拜拜