HarmonyOS7 ArkUI 图片九宫格切割 - 正方形裁剪预览与发布效果实战:从状态到页面反馈

文章目录

      • 前言
      • 先把业务看明白
      • 状态和 UI 的关系
      • 核心逻辑拆读
        • 片段 1:`get cols(): number {`
        • 片段 2:`get currentFilter(): FilterPreset_wc1v {`
      • 使用方式
      • 完整代码
      • 一个小复盘

前言

这个案例看着是一个小页面,里面其实塞了不少常见业务写法:列表、筛选、按钮事件、条件样式,基本都能练到。
它的主线是图片九宫格切割,细节落在正方形裁剪预览与发布效果。这些细节刚好能把页面状态、用户操作和组件刷新串起来。

先把业务看明白

NineGridImageSplitPage不是一个只展示静态内容的页面。它会根据用户点击、输入、切换或计时来改变界面,所以阅读代码时可以先抓三条线:

观察点在这个案例里的表现
页面主题图片九宫格切割
主要文案原图,清新,复古,黑白,暖调,冷调
常用组件Button,Column,Divider,ForEach,Grid,GridItem,Row,Scroll,Slider,Stack,Text
适合练习状态驱动 UI、条件样式、列表渲染、事件回调

状态和 UI 的关系

ArkUI 的页面刷新依赖状态变化。下面这些@State字段就是页面的“开关”和“数据源”,不用手动找 DOM,也不用自己通知某个控件刷新。

状态字段类型我会怎么理解它
selectedCountnumber当前选中项,决定高亮和内容切换
filterIndexnumber当前选中项,决定高亮和内容切换
previewModeboolean记录页面当前选择、输入或展示状态
spacingnumber记录页面当前选择、输入或展示状态

一个经验:先把@State看完,再看build(),页面逻辑会清楚很多。否则很容易被一长串布局代码带偏。

核心逻辑拆读

片段 1:get cols(): number {

这段代码承担的是页面里的“判断”或“动作”,把它从布局里抽出来后,build()会清爽很多。后面要加新条件,也不会到处翻 UI 代码。

getcols():number{if(this.selectedCount<=1)return1if(this.selectedCount<=4)return2return3}
片段 2:get currentFilter(): FilterPreset_wc1v {

这段代码承担的是页面里的“判断”或“动作”,把它从布局里抽出来后,build()会清爽很多。后面要加新条件,也不会到处翻 UI 代码。

getcurrentFilter():FilterPreset_wc1v{returnthis.filterPresets[this.filterIndex]}

使用方式

把下面代码放进 ArkTS 页面文件中即可运行。用于正式项目时,我会继续把数据模型、卡片 Builder 和页面事件拆出去,页面文件只负责组合。

建议练习顺序:先改状态字段 -> 再改交互事件 -> 最后调整 UI 样式 检查重点:点击是否刷新、列表 key 是否稳定、条件样式是否集中管理
这份代码可以继续扩展的方向
  • 把模拟数据替换成接口数据
  • 抽出复用卡片组件
  • 增加空状态和异常状态
  • 补充深色模式或主题色适配
  • 对输入类场景增加校验提示

完整代码

// NineGridImageSplitPage - 图片九宫格切割 - 正方形裁剪预览与发布效果interfaceGridCell{row:numbercol:numberbgX:numberbgY:number}interfaceFilterPreset_wc1v{name:stringbrightness:numbercontrast:numbersaturate:numbergrayscale:number}@Entry@Componentstruct NineGridImageSplitPage{@StateselectedCount:number=9@StatefilterIndex:number=0@StatepreviewMode:boolean=false@Statespacing:number=4privatefilterPresets:FilterPreset_wc1v[]=[{name:'原图',brightness:1.0,contrast:1.0,saturate:1.0,grayscale:0},{name:'清新',brightness:1.1,contrast:0.9,saturate:1.3,grayscale:0},{name:'复古',brightness:0.9,contrast:1.1,saturate:0.7,grayscale:0.2},{name:'黑白',brightness:1.0,contrast:1.2,saturate:0,grayscale:1.0},{name:'暖调',brightness:1.05,contrast:1.0,saturate:1.2,grayscale:0},{name:'冷调',brightness:0.95,contrast:1.1,saturate:0.9,grayscale:0},]privatecolorPalette:string[]=['#FF6B9D','#C44569','#F8B500','#6C5CE7','#00B894','#0984E3','#E17055','#74B9FF','#A29BFE',]getcols():number{if(this.selectedCount<=1)return1if(this.selectedCount<=4)return2return3}getcurrentFilter():FilterPreset_wc1v{returnthis.filterPresets[this.filterIndex]}getCells():GridCell[]{constcells:GridCell[]=[]for(letr=0;r<this.cols;r++){for(letc=0;c<this.cols;c++){if(cells.length<this.selectedCount){cells.push({row:r,col:c,bgX:c,bgY:r})}}}returncells}build(){Column({space:0}){// 顶部导航Row(){Text('✕').fontSize(20).fontColor('#1a1a1a')Blank()Text('九宫格切割').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')Blank()Text('发布').fontSize(16).fontColor('#1890ff').fontWeight(FontWeight.Bold)}.width('100%').padding({left:16,right:16,top:14,bottom:14}).backgroundColor('#ffffff')Scroll(){Column({space:20}){// 源图预览(用颜色渐变模拟)Column(){Text('源图预览').fontSize(13).fontColor('#999999').padding({bottom:10}).width('100%')Stack(){Column().width('100%').height(280).linearGradient({angle:135,colors:[['#FF6B9D',0],['#C44569',0.3],['#F8B500',0.6],['#6C5CE7',1.0]]}).borderRadius(12).brightness(this.currentFilter.brightness).contrast(this.currentFilter.contrast).saturate(this.currentFilter.saturate).grayscale(this.currentFilter.grayscale)// 九宫格切割线if(this.selectedCount===9){Column(){Divider().strokeWidth(2).color('#ffffff80').margin({top:93})Divider().strokeWidth(2).color('#ffffff80').margin({top:93})}.width('100%').height(280).position({x:0,y:0})Row(){Divider().width(2).height(280).backgroundColor('#ffffff80').margin({left:'33%'})Divider().width(2).height(280).backgroundColor('#ffffff80').margin({left:'33%'})}.width('100%').height(280).position({x:0,y:0})}}.width('100%')}.padding({left:16,right:16})// 滤镜选择Column({space:10}){Text('滤镜效果').fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1a1a1a').padding({left:16}).width('100%')Scroll(){Row({space:12}){ForEach(this.filterPresets,(f:FilterPreset_wc1v,idx:number)=>{Column({space:6}){Column().width(60).height(60).linearGradient({angle:135,colors:[['#FF6B9D',0],['#6C5CE7',1.0]]}).borderRadius(8).brightness(f.brightness).contrast(f.contrast).saturate(f.saturate).grayscale(f.grayscale).border({width:2,color:this.filterIndex===idx?'#1890ff':'transparent'})Text(f.name).fontSize(11).fontColor(this.filterIndex===idx?'#1890ff':'#666666')}.alignItems(HorizontalAlign.Center).onClick(()=>{this.filterIndex=idx})})}.padding({left:16,right:16})}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)}// 切割数量选择Column({space:12}){Text('切割数量').fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1a1a1a').padding({left:16}).width('100%')Row({space:12}){ForEach([1,2,3,4,6,9],(n:number)=>{Column({space:4}){// 预览小网格Column(){Grid(){ForEach(Array.from({length:n}),(_:Object,i:number)=>{GridItem(){Column().width('100%').height('100%').backgroundColor(this.colorPalette[i%this.colorPalette.length])}})}.columnsTemplate(n<=1?'1fr':n<=4?'1fr 1fr':'1fr 1fr 1fr').rowsGap(2).columnsGap(2).width(36).height(36)}.width(44).height(44).backgroundColor(this.selectedCount===n?'#e8f4ff':'#f5f5f5').borderRadius(8).border({width:2,color:this.selectedCount===n?'#1890ff':'transparent'}).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)Text(`${n}`).fontSize(11).fontColor(this.selectedCount===n?'#1890ff':'#666666')}.alignItems(HorizontalAlign.Center).onClick(()=>{this.selectedCount=n})})}.justifyContent(FlexAlign.SpaceEvenly).width('100%')}// 间距设置Column({space:8}){Row(){Text('切割间距').fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1a1a1a')Blank()Text(`${this.spacing}px`).fontSize(13).fontColor('#1890ff')}.padding({left:16,right:16}).width('100%')Slider({value:this.spacing,min:0,max:20,step:1}).width('90%').trackColor('#e8e8e8').selectedColor('#1890ff').onChange((v:number)=>{this.spacing=Math.round(v)})}// 切割预览Column({space:12}){Text('切割预览').fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1a1a1a').padding({left:16}).width('100%')Grid(){ForEach(this.getCells(),(cell:GridCell,idx:number)=>{GridItem(){Column().width('100%').height('100%').linearGradient({angle:135,colors:[['#FF6B9D',0],['#C44569',0.3],['#F8B500',0.6],['#6C5CE7',1.0]]}).brightness(this.currentFilter.brightness).contrast(this.currentFilter.contrast).saturate(this.currentFilter.saturate).grayscale(this.currentFilter.grayscale)}})}.columnsTemplate(this.cols===1?'1fr':this.cols===2?'1fr 1fr':'1fr 1fr 1fr').rowsGap(this.spacing).columnsGap(this.spacing).width('100%').height(this.cols===1?300:this.cols===2?200:300).padding({left:16,right:16})}// 发布按钮Button('保存并发布').width('90%').height(50).fontSize(16).backgroundColor('#1890ff').borderRadius(25).fontWeight(FontWeight.Medium).margin({top:8})Column().height(32)}.padding({top:16})}.layoutWeight(1).backgroundColor('#f5f7fa')}.width('100%').height('100%').backgroundColor('#f5f7fa')}}

一个小复盘

这个案例可以当成一个小模板:先把页面会变的东西放进状态,再用函数或 Builder 处理重复逻辑,最后让布局只关心展示。写 HarmonyOS7 页面时,这个顺序通常比一上来堆 UI 更稳。