news 2026/4/23 13:46:27

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

作者头像

张小明

前端开发工程师

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

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

Flutter: 3.35.7

前面我们实现了元素的变换操作,单纯的变换操作只是为了后续功能的实现,接下来我们就开始扩展容器的属性。

我们要新增容器功能的扩展,那么就要划分新的区域来实现这部分功能,所以我们得重新规划和计算。

有许多方式实现扩展功能区域,第一种就是划分区域,划分下面为属性扩展区域,元素变换区域则会相应的压缩,所以涉及到变换的计算有使用到容器宽高属性的都要变化;第二种就是将功能区域设计成一个底部弹框覆盖在元素变换区域,在元素变换过程中隐藏,未变换就展示,这样就不用更改,不过得制定弹出时机。这里我们使用第一种,感兴趣的可以自行研究第二种。

常量新增配置:

/// 底部功能区域的高度staticconstdouble bottomHeight=100;/// 变换区域的左右marginstaticconstdouble transformMargin=20;

重新计算宽高、重新设计布局和更改变换过程中应用到容器宽高的计算(将变换计算中的_containerWidth换成_transformWidth,_containerHeight换成_transformHeight):

/// 变换区域的宽doubleget_transformWidth{return_width-ConstantsConfig.transformMargin*2;}/// 变换区域的高doubleget_transformHeight{return_height-ConstantsConfig.bottomHeight;}/// 最终容器的宽doubleget_width{return_containerWidth==0?(widget.containerWidth??double.infinity):_containerWidth;}/// 最终容器的高doubleget_height{return_containerHeight==0?(widget.containerHeight??double.infinity):_containerHeight;}SizedBox(key:_multipleTransformContainerGlobalKey,width:_width,height:_height,child:_containerWidth==0||_containerHeight==0?null:Column(children:[// 变换区域GestureDetector(// 其他省略...child:Container(width:_transformWidth,height:_transformHeight,margin:EdgeInsets.symmetric(horizontal:ConstantsConfig.transformMargin,),color:Colors.white,child:_containerWidth==0||_containerHeight==0?null:Stack(// 其他省略...),),),// 底部功能区域Container(height:ConstantsConfig.bottomHeight,color:Colors.white60,),],),);

运行效果:

顺便将使用外层的容器设置了顶部边距。

后续的功能扩展就在这个小小的区域上面实现。规划完区域,我们就要对变换元素做出修改,总不可能一直操作一个矩形吧;按照部分经验,这种功能操作的大多数是图片+文本,所以我们以这两种来划分元素的类型为例,后续如果有新的类型再增加即可。

新增元素类型:

enumElementType{/// 图片imageType(type:'image'),/// 文本textType(type:'text'),;finalString type;constElementType({requiredthis.type});}classElementModel{// 其他省略.../// 元素的类型finalString type;/// 如果是元素是图片类型,图片的pathfinalString?imagePath;/// 如果元素是文本类型,文本属性finalElementTextOptions?textOptions;// 其他省略...}// 其他省略...enumTextAlignType{left(type:'left',textAlign:TextAlign.left),right(type:'right',textAlign:TextAlign.right),center(type:'center',textAlign:TextAlign.center),justify(type:'justify',textAlign:TextAlign.justify),;finalString?type;finalTextAlign textAlign;constTextAlignType({requiredthis.type,requiredthis.textAlign,});}classElementTextOptions{constElementTextOptions({requiredthis.text,this.textHeight=ConstantsConfig.initFontHeight,this.fontSize=ConstantsConfig.initFontSize,this.fontColor=Colors.black,this.fontWeight,this.fontFamily,this.textAlign=ConstantsConfig.initFontAlign,this.letterSpacing,});/// 文本内容finalString text;/// 文本行高finaldouble textHeight;/// 文本大小finaldouble fontSize;/// 文本颜色finalColor fontColor;/// 文本字重(100-1000,1000就是bold)finalint?fontWeight;/// 文本字体finalString?fontFamily;/// 文本对齐方式finalString?textAlign;/// 文本字间距finaldouble?letterSpacing;ElementTextOptionscopyWith({String?text,double?textHeight,double?fontSize,Color?fontColor,int?fontWeight,String?fontFamily,String?textAlign,double?letterSpacing,}){returnElementTextOptions(text:text??this.text,textHeight:textHeight??this.textHeight,fontSize:fontSize??this.fontSize,fontColor:fontColor??this.fontColor,fontWeight:fontWeight??this.fontWeight,fontFamily:fontFamily??this.fontFamily,textAlign:textAlign??this.textAlign,letterSpacing:letterSpacing??this.letterSpacing,);}}

定义完属性,我们就开始新增图片元素,在功能区新增图片选择按钮,从本地文件中选择图片,所以我们得增加图片选择插件(image_picker),在获取到图片的时候再将部分必要信息填充,然后将选择的图片添加到元素列表中即可:

// 其他省略...classImageElementAddextendsStatefulWidget{constImageElementAdd({super.key,requiredthis.transformWidth,requiredthis.transformHeight,requiredthis.addElement,});/// 变换区域的宽,用于计算选择图片的初始宽度finaldouble transformWidth;/// 变换区域的高,用于计算选择图片的初始高度finaldouble transformHeight;/// 新增元素方法,用于将选择的图片添加到元素列表中finalFunction(ElementModel)addElement;@overrideState<ImageElementAdd>createState()=>_ImageElementAddState();}class_ImageElementAddStateextendsState<ImageElementAdd>{/// 选择图片Future<void>_imagePicker()async{finalImagePicker picker=ImagePicker();finalXFile?imageFile=awaitpicker.pickImage(source:ImageSource.gallery);if(imageFile!=null){finalimageInfo=await_loadImageFromFile(imageFile.path);widget.addElement(ElementModel(id:DateTime.now().millisecondsSinceEpoch,elementWidth:imageInfo.$1,elementHeight:imageInfo.$2,type:ElementType.imageType.type,imagePath:imageFile.path,));}}/// 从本地文件加载图片并获取宽高////// 通过[filePath]获取这张图片的宽高Future<(double,double)>_loadImageFromFile(String filePath)async{finalfile=File(filePath);finalbytes=awaitfile.readAsBytes();finalcodec=awaitui.instantiateImageCodec(bytes);finalframe=awaitcodec.getNextFrame();finalimageInfo=frame.image;finaldouble imageWidth=imageInfo.width.toDouble();finaldouble imageHeight=imageInfo.height.toDouble();finaldouble tempContainerWidth=widget.transformWidth/2;finaldouble tempContainerHeight=widget.transformHeight/2;double tempWidth=imageWidth;double tempHeight=imageHeight;// 以长边来设置图片的最终初始宽高if(imageWidth>=imageHeight){tempWidth=imageWidth>tempContainerWidth?tempContainerWidth:imageWidth;tempHeight=(tempWidth/imageWidth)*imageHeight;}else{tempHeight=imageHeight>tempContainerHeight?tempContainerHeight:imageHeight;tempWidth=(tempHeight/imageHeight)*imageWidth;}return(tempWidth,tempHeight);}@overrideWidgetbuild(BuildContext context){returnElevatedButton(onPressed:_imagePicker,child:Text('图片选择'),);}}

运行效果:

这样我们就简单实现了图片元素的新增。接下来我们简单实现文本元素的新增。文本元素就需要考虑多些了,因为涉及到文本属性的修改,新增的时候将对应的属性修改放开,后续也涉及到编辑,所以封装成一个部件,为了后续能更好的展示,我们封装成一个Positioned,通过控制状态来展示:

// 其他省略...classTextOptionsextendsStatefulWidget{constTextOptions({super.key,requiredthis.transformWidth,requiredthis.isShow,requiredthis.addElement,});/// 变换区域的宽,用于计算选择文本元素的最大宽度finaldouble transformWidth;/// 文本元素属性部件是否展示finalbool isShow;/// 新增元素方法,用于新增文本部件finalFunction(ElementModel)addElement;@overrideState<TextOptions>createState()=>_TextOptionsState();}class_TextOptionsStateextendsState<TextOptions>{/// 新增文本元素void_onAddTextElement(String text){}@overrideWidgetbuild(BuildContext context){returnAnimatedPositioned(duration:Duration(milliseconds:100),left:0,right:0,bottom:widget.isShow?0:-ConstantsConfig.fontOptionsWidgetHeight,child:Container(padding:EdgeInsets.all(20),color:Colors.white,height:ConstantsConfig.fontOptionsWidgetHeight,child:TextField(style:TextStyle(fontSize:15,fontWeight:FontWeight.w600,height:1.333,),decoration:InputDecoration(isCollapsed:true,contentPadding:EdgeInsets.zero,border:InputBorder.none,counter:constOffstage(),hintText:'请输入',hintStyle:TextStyle(fontSize:15,fontWeight:FontWeight.w600,height:1.333,),),onSubmitted:_onAddTextElement,),),);}}
// 其他省略...classTextElementAddextendsStatefulWidget{constTextElementAdd({super.key,requiredthis.onShowTextOptions,});/// 展示文本属性部件finalFunction(bool)onShowTextOptions;@overrideState<TextElementAdd>createState()=>_TextElementAddState();}class_TextElementAddStateextendsState<TextElementAdd>{void_onShowText(){widget.onShowTextOptions(true);}@overrideWidgetbuild(BuildContext context){returnElevatedButton(onPressed:_onShowText,child:Text('文本',style:TextStyle(fontSize:12,),),);}}

运行效果:

接下来就在这个基础上实现新增文本的逻辑。首先,新增文本的时候我也也要得到这个字符串应该拥有的宽高。通过 flutter 提供的 TextPainter 来获取:

/// 计算文本的宽高////// 传入文本字符串[text]、文本的样式[style]和最大的宽度[maxWidth]来计算文本的宽高static(double,double)calculateTextSize({required String text,required TextStyle style,required double maxWidth}){if(text.isEmpty){return(0,0);}finalTextPainter textPainter=TextPainter(text:TextSpan(text:text,style:style),textDirection:TextDirection.ltr,)..layout(maxWidth:maxWidth);finaltempWidth=textPainter.width;finaltempHeight=textPainter.height;// 不能小于最小值finalminSize=ConstantsConfig.minSize;return(tempWidth<=minSize?minSize:tempWidth,tempHeight<=minSize?minSize:tempHeight);}

获取到文本元素的宽高后,就可以实现新增的逻辑了:

/// 新增文本元素void_onAddTextElement(String text){// 一些初始化的文本属性TextStyle style=TextStyle(fontSize:ConstantsConfig.initFontSize,height:ConstantsConfig.initFontHeight,);final(tempWidth,tempHeight)=TransformUtils.calculateTextSize(text:text,style:style,maxWidth:widget.transformWidth,);widget.addElement(ElementModel(id:DateTime.now().millisecondsSinceEpoch,elementHeight:tempHeight,elementWidth:tempWidth,type:ElementType.textType.type,textOptions:ElementTextOptions(text:text),));}

运行效果:

这样我们就简单实现了新增文本元素,下面就来设计文本元素属性的修改。因为属性比较多,我们可以使用tab来分开(前面简单封装过一个tab,感兴趣的朋友可以看看),也可以使用滑动组件,这里为了方便,所以使用滑动组件(我们以行高属性为例,其他的实现类似,只是结构稍微调整即可):

/// 设置文本的属性void_setTextOptions(ElementTextOptions textOptions){if(_currentElement?.type==ElementType.textType.type){TextStyle style=TextStyle(fontSize:textOptions.fontSize,height:textOptions.textHeight,letterSpacing:textOptions.letterSpacing,fontWeight:TransformUtils.getFontWeight(textOptions.fontWeight,),);final(tempWidth,tempHeight)=TransformUtils.calculateTextSize(text:textOptions.text,style:style,maxWidth:_currentElement!.elementWidth,);_currentElement=_currentElement?.copyWith(// elementWidth: tempWidth,elementHeight:tempHeight,textOptions:_currentElement?.textOptions?.copyWith(text:textOptions.text,textHeight:textOptions.textHeight,fontSize:textOptions.fontSize,fontColor:textOptions.fontColor,fontWeight:textOptions.fontWeight,fontFamily:textOptions.fontFamily,textAlign:textOptions.textAlign,letterSpacing:textOptions.letterSpacing,),);_onChange();}}
void_onReduceFontHeight(){if(widget.textOptions!=null&&widget.textOptions!.textHeight>0){widget.setTextOptions(widget.textOptions!.copyWith(textHeight:(Decimal.parse('${widget.textOptions!.textHeight}')-Decimal.parse('0.1')).toDouble(),),);}}void_onAddFontHeight(){if(widget.textOptions!=null){widget.setTextOptions(widget.textOptions!.copyWith(textHeight:(Decimal.parse('${widget.textOptions!.textHeight}')+Decimal.parse('0.1')).toDouble(),),);}}

这样我们就简单实现了属性的修改,样式什么的后面有时间再慢慢调整,现在只是功能为主,毕竟真实的开发总会有UI的。

下面快速预览一下文本属性修改的完整效果:

字体因为难得找相关的所以就暂未实现。

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

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

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

从零开始理解I2S协议工作原理:音频设备入门必看

深入理解 I2S 协议&#xff1a;从音频传输原理到实战设计你有没有遇到过这样的情况&#xff1f;明明代码写得没问题&#xff0c;音频芯片也供电正常&#xff0c;可耳机里传出来的却是“咔哒咔哒”的杂音&#xff0c;甚至左右声道还对调了。如果你正在做一块带音频输出的嵌入式板…

作者头像 李华
网站建设 2026/4/20 21:07:13

零基础也能懂的ESP32连接阿里云MQTT讲解

从零开始&#xff0c;用一块ESP32点亮你的“云控灯” 你有没有想过&#xff0c;让家里的小风扇在温度过高时自动启动&#xff1f;或者远程查看阳台花盆的土壤湿度&#xff1f;这些看似复杂的智能场景&#xff0c;其实只需要一块 ESP32 和一个云端平台就能实现。而连接它们之…

作者头像 李华
网站建设 2026/4/14 0:45:22

工业自动化设备PCB布线可制造性设计:DFM实践指南

工业自动化设备PCB布线的可制造性设计&#xff1a;从图纸到量产的实战经验在工业控制领域&#xff0c;一块小小的PCB板子&#xff0c;往往承载着整台设备的“神经中枢”。主控芯片、信号调理电路、电源模块、通信接口……所有这些功能都集成在几平方厘米的空间里。然而&#xf…

作者头像 李华
网站建设 2026/4/22 11:22:57

基于ESP32的OTG主机模式实验教程:新手必看

手把手教你用ESP32玩转USB OTG主机模式&#xff1a;从点灯到读U盘的硬核实战你有没有想过&#xff0c;让一块小小的ESP32像电脑一样“插上键盘就能打字”、“接个U盘直接读文件”&#xff1f;这听起来像是高级嵌入式系统的专属能力&#xff0c;但其实——只要用对型号、写对代码…

作者头像 李华
网站建设 2026/4/22 21:14:58

PetaLinux内核定制全流程:新手入门必看图文教程

从零开始玩转PetaLinux&#xff1a;一次完整的内核定制实战之旅 你有没有遇到过这样的场景&#xff1f; 手头一块Zynq开发板&#xff0c;Vivado工程已经跑通了AXI GPIO和ADC IP&#xff0c;但Linux系统就是“看不见”这些外设&#xff1b;或者内核启动卡在串口输出一半&#…

作者头像 李华
网站建设 2026/4/23 0:16:48

AD导出Gerber文件与钻孔文件同步输出技巧(操作指南)

AD导出Gerber与钻孔文件&#xff1a;如何一次做对&#xff0c;避免制板返工&#xff1f; 你有没有遇到过这样的情况&#xff1f; PCB设计明明在Altium Designer里看起来完美无瑕&#xff0c;发给厂家后却收到反馈&#xff1a;“丝印反了”、“孔位偏移0.1mm”、“缺钻孔文件”…

作者头像 李华