2026/2/14 17:30:19
网站建设
项目流程
微信网站设计欣赏,淮北建设信息网,建立免费公司网站,一条龙平台Flutter 实现一个容器内部元素可平移、缩放和旋转等功能#xff08;四#xff09;
Flutter: 3.35.6
前面我们实现了单个元素的#xff0c;现在实现多个元素的。因为有前面功能的落地实现#xff0c;我们也可以对于部分属性的提前抽取#xff0c;部分数据模型的提前封装。…Flutter 实现一个容器内部元素可平移、缩放和旋转等功能四Flutter: 3.35.6前面我们实现了单个元素的现在实现多个元素的。因为有前面功能的落地实现我们也可以对于部分属性的提前抽取部分数据模型的提前封装。还是按照简单到复杂的实现思路我们先对容器部分进行简单分析。前面也提到最后的手势操作提升到容器因为对比给每个子元素设置手势这样的内存开销会减小很多目前容器的基础属性有宽和高后期如果需要新的属性直接再添加即可importpackage:flutter/material.dart;classMultipleTransformContainerextendsStatefulWidget{constMultipleTransformContainer({super.key,this.containerWidth,this.containerHeight,});/// 容器的宽不传默认为父容器的最大宽度finaldouble?containerWidth;/// 容器的高不传默认为父容器的最大高度finaldouble?containerHeight;overrideStateMultipleTransformContainercreateState()_MultipleTransformContainerState();}class_MultipleTransformContainerStateextendsStateMultipleTransformContainer{/// 按下事件void_onPanDown(DragDownDetails details){}/// 按下移动事件void_onPanUpdate(DragUpdateDetails details){}/// 结束事件void_onPanEnd(){}overrideWidgetbuild(BuildContext context){returnGestureDetector(onPanDown:_onPanDown,onPanUpdate:_onPanUpdate,onPanEnd:(details)_onPanEnd(),onPanCancel:_onPanEnd,child:Container(width:widget.containerWidth??double.infinity,height:widget.containerHeight??double.infinity,color:Colors.transparent,),);}}接下来对子元素进行简单分析。子元素主要分为三个部分一个是自身的属性随着变换操作而变化一个是中间临时的变量值响应单次事件过程中需要初始化和中间临时改变的值一个是操作的区域响应变换的事件。结合前面的单个案例我们可以提取子元素的部分属性元素宽度一般来说元素的宽属性为必传如果有默认值可能会导致后期元素拉伸所以限制为必传元素高度和宽一样元素的x坐标坐标就可以设置初始的默认值了因为不会对元素自身形成拉伸压缩效果元素的y坐标和x一样旋转角度和x一样id用于确定当前操作的元素import../configs/constants_config.dart;classElementModel{constElementModel({requiredthis.id,requiredthis.elementWidth,requiredthis.elementHeight,this.xConstantsConfig.initX,this.yConstantsConfig.initY,this.rotationAngleConstantsConfig.initRotationAngle,});/// 当前元素的唯一idfinalint id;/// 元素的宽finaldouble elementWidth;/// 元素的高finaldouble elementHeight;/// 元素的x坐标finaldouble x;/// 元素的y坐标finaldouble y;/// 元素的旋转角度finaldouble rotationAngle;ElementModelcopyWith({double?elementWidth,double?elementHeight,double?x,double?y,double?rotationAngle,}){returnElementModel(id:id,elementWidth:elementWidth??this.elementWidth,elementHeight:elementHeight??this.elementHeight,x:x??this.x,y:y??this.y,rotationAngle:rotationAngle??this.rotationAngle,);}}/// 用于设置一些初始化值classConstantsConfig{/// 元素的初始化x坐标staticconstdouble initX10;/// 元素的初始化y坐标staticconstdouble initY10;/// 元素的初始化旋转角度staticconstdouble initRotationAngle0;}结合前面的案例我们抽取临时中间变量如下x坐标单次操作开始时的x坐标同上次操作结束时的x坐标y坐标逻辑和x一样旋转角度逻辑和x一样操作状态值/// 元素当前操作状态enumElementStatus{move,rotate,scale,}/// 元素的临时中间变量classTemporaryModel{constTemporaryModel({requiredthis.x,requiredthis.y,requiredthis.rotationAngle,this.status,});/// 单次操作完成时的初始x坐标finaldouble x;/// 单次操作完成时的初始y坐标finaldouble y;/// 单次操作完成时的初始旋转角度finaldouble rotationAngle;/// 对应的元素的操作状态finalElementStatus?status;TemporaryModelcopyWith({double?x,double?y,double?rotationAngle,ElementStatus?status,}){returnTemporaryModel(x:x??this.x,y:y??this.y,rotationAngle:rotationAngle??this.rotationAngle,status:status??this.status,);}}接下来就是控制操作区域其实在使用 javascript 实现该功能的时候也分析过所以这里直接基于这个来做一个简单的说明难免会站在上帝视角。因为常规来说控制的区域位于元素容器的四个顶点处如果我们也想要自定义去他区域就要给出相应的计算区域的方式这里给出一种确定响应区域的计算方式基于元素本身创建一个坐标系坐标原点为元素的左上角使用元素的总体宽高和响应区域中心点来计算出一个比例通过这个比例就能让我们使用区域内包括区域外的任意区域来做响应的区域例如元素整体宽高为20*20我需要响应区域的中心点在右上角(20, 0)所以这个比例就是 x: 20/20y: 0/20。计算方式有了下面就该确定响应区域的样式常规来说一般就是一张图片我们前期就以图片为主后面就当作扩展功能允许自定义。最后一点就是该响应区域的触发方式是什么例如有些操作是响应点击操作删除镜像等等有些操作是响应按下移动操作移动缩放旋转等等所以我们还需要一个触发方式。基于此我们开始抽取响应区域importelement_model.dart;enumTriggerMethod{move,down,;}classResponseAreaModel{constResponseAreaModel({requiredthis.areaWidth,requiredthis.areaHeight,requiredthis.xRatio,requiredthis.yRatio,requiredthis.status,requiredthis.icon,requiredthis.trigger,});/// 响应区域的宽finaldouble areaWidth;/// 响应区域的高finaldouble areaHeight;/// 响应区域的比例横向finaldouble xRatio;/// 响应区域的比例竖向finaldouble yRatio;/// 响应区域应该响应什么操作finalElementStatus status;/// 响应区域的iconfinalString icon;/// 当前响应操作的触发方式finalTriggerMethod trigger;}前期的准备工作差不多就完成了下面我们简单来实现一个元素的移动。现在是多个元素的当前正在操作的肯定只有一个元素所以按下的时候得选中元素后续的操作就是作用于选中的元素因为还只是移动操作所以也先不考虑旋转。因为我们将容器的宽高设置成了可不传但是我们操作过程中可能对于边界值需要用到容器的宽高做计算所以备份一份如果没有传递则通过GlobalKey去获取容器的宽高importpackage:flutter/material.dart;importmodels/element_model.dart;importtransform_item.dart;classMultipleTransformContainerextendsStatefulWidget{constMultipleTransformContainer({super.key,this.containerWidth,this.containerHeight,});/// 容器的宽不传默认为父容器的最大宽度finaldouble?containerWidth;/// 容器的高不传默认为父容器的最大高度finaldouble?containerHeight;overrideStateMultipleTransformContainercreateState()_MultipleTransformContainerState();}class_MultipleTransformContainerStateextendsStateMultipleTransformContainer{/// 用于获取容器的宽高finalGlobalKey _multipleTransformContainerGlobalKeyGlobalKey();finalListElementModel_elementList[ElementModel(id:DateTime.now().microsecondsSinceEpoch,elementWidth:100,elementHeight:100,),];/// 记录一份容器的宽高用于没传递的时候有个真实的容器宽高double _containerWidth0;double _containerHeight0;/// 当前选中的元素ElementModel?_currentElement;/// 临时的中间变量用于计算TemporaryModel?_temporary;/// 开始点击的位置Offset _startPositionOffset(0,0);overridevoidinitState(){super.initState();WidgetsBinding.instance.addPostFrameCallback((_){_getContainerSize();});}overridevoiddispose(){_multipleTransformContainerGlobalKey.currentState?.dispose();super.dispose();}/// 获取容器的宽高属性用于没传递容器宽高的时候有个真实的容器宽高void_getContainerSize(){double tempWidth0;double tempHeight0;if(widget.containerHeight!nullwidget.containerWidth!null){tempHeightwidget.containerHeight!;tempWidthwidget.containerWidth!;}else{tempWidth_multipleTransformContainerGlobalKey.currentContext?.size?.width??0;tempHeight_multipleTransformContainerGlobalKey.currentContext?.size?.height??0;}setState((){_containerHeighttempHeight;_containerWidthtempWidth;});}/// 按下事件void_onPanDown(DragDownDetails details){finaldxdetails.localPosition.dx;finaldydetails.localPosition.dy;ElementModel?currentElement;TemporaryModel tempTemporaryModel(x:0,y:0,rotationAngle:0);// 遍历判断当前点击的位置是否落在了某个元素的响应区域for(varitemin_elementList){finalstatus_onDownZone(x:dx,y:dy,item:item);if(status!null){currentElementitem;temptemp.copyWith(status:status);break;}}if(currentElement!null){// 如果点击的区域存在元素并且点击区域存在的元素和当前选中的元素不是一个// 则选中该元素并设置其部分初始化属性if(_currentElement?.id!currentElement.id){_currentElementcurrentElement;}_temporarytemp.copyWith(x:currentElement.x,y:currentElement.y,);_startPositionOffset(dx,dy);setState((){});}else{// 如果点击的区域不存在元素并且当前选中的元素不为null则置空选中if(_currentElement!null){_currentElementnull;_temporarynull;setState((){});}}}/// 按下移动事件void_onPanUpdate(DragUpdateDetails details){if(_currentElementnull||_temporarynull)return;if(_temporary?.statusElementStatus.move){_onMove(x:details.localPosition.dx,y:details.localPosition.dy);}}/// 结束事件void_onPanEnd(){}/// 处理元素移动void_onMove({required double x,required double y}){if(_currentElementnull||_temporarynull)return;double tempX_temporary!.xx-_startPosition.dx;double tempY_temporary!.yy-_startPosition.dy;// 限制左边界if(tempX0){tempX0;}// 限制右边界if(tempX_containerWidth-_currentElement!.elementWidth){tempX_containerWidth-_currentElement!.elementWidth;}// 限制上边界if(tempY0){tempY0;}// 限制下边界if(tempY_containerHeight-_currentElement!.elementHeight){tempY_containerHeight-_currentElement!.elementHeight;}_currentElement_currentElement!.copyWith(x:tempX,y:tempY,);_onChange();}/// 当前元素属性变化的时候更新列表中对应元素的属性void_onChange(){if(_currentElementnull||_temporarynull)return;for(vari0;i_elementList.length;i){finalitem_elementList[i];if(item.id_currentElement?.id){_elementList[i]item.copyWith(x:_currentElement?.x,y:_currentElement?.y,);setState((){});break;}}}/// 判断点击的区域////// 以传入的[item]元素为参考/// 判断当前点击的坐标[x]和[y]落在[item]元素的哪个响应区域ElementStatus?_onDownZone({required double x,required double y,required ElementModel item,}){if(xitem.xxitem.elementWidthitem.xyitem.yyitem.elementHeightitem.y){// 判断移动区域目前没有考虑元素的旋转returnElementStatus.move;}returnnull;}overrideWidgetbuild(BuildContext context){returnGestureDetector(onPanDown:_onPanDown,onPanUpdate:_onPanUpdate,onPanEnd:(details)_onPanEnd(),onPanCancel:_onPanEnd,child:Container(key:_multipleTransformContainerGlobalKey,width:widget.containerWidth??double.infinity,height:widget.containerHeight??double.infinity,color:Colors.transparent,child:_containerWidth0||_containerHeight0?null:Stack(children:[..._elementList.map((item)TransformItem(elementItem:item,selected:item.id_currentElement?.id,)),],),),);}}importpackage:flutter/material.dart;importmodels/element_model.dart;/// 抽取渲染的元素classTransformItemextendsStatelessWidget{constTransformItem({super.key,requiredthis.elementItem,requiredthis.selected});finalElementModel elementItem;finalbool selected;overrideWidgetbuild(BuildContext context){returnPositioned(left:elementItem.x,top:elementItem.y,child:Container(width:elementItem.elementWidth,height:elementItem.elementHeight,decoration:BoxDecoration(color:selected?Colors.amberAccent:Colors.blueAccent,),),);}}运行效果这样就简单实现了元素的移动效果代码还要很大的优化空间不着急我们一步一步来。感兴趣的也可以关注我的微信公众号【前端学习小营地】不定时会分享一些小功能今天的分享就到此结束了感谢阅读拜拜