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

先把业务看明白
NineGridImageSplitPage 不是一个只展示静态内容的页面。它会根据用户点击、输入、切换或计时来改变界面,所以阅读代码时可以先抓三条线:
| 观察点 | 在这个案例里的表现 |
|---|---|
| 页面主题 | 图片九宫格切割 |
| 主要文案 | 原图, 清新, 复古, 黑白, 暖调, 冷调 |
| 常用组件 | Button, Column, Divider, ForEach, Grid, GridItem, Row, Scroll, Slider, Stack, Text |
| 适合练习 | 状态驱动 UI、条件样式、列表渲染、事件回调 |
状态和 UI 的关系
ArkUI 的页面刷新依赖状态变化。下面这些 @State 字段就是页面的“开关”和“数据源”,不用手动找 DOM,也不用自己通知某个控件刷新。
| 状态字段 | 类型 | 我会怎么理解它 |
|---|---|---|
selectedCount |
number |
当前选中项,决定高亮和内容切换 |
filterIndex |
number |
当前选中项,决定高亮和内容切换 |
previewMode |
boolean |
记录页面当前选择、输入或展示状态 |
spacing |
number |
记录页面当前选择、输入或展示状态 |
一个经验:先把
@State看完,再看build(),页面逻辑会清楚很多。否则很容易被一长串布局代码带偏。
核心逻辑拆读
片段 1:get cols(): number {
这段代码承担的是页面里的“判断”或“动作”,把它从布局里抽出来后,build() 会清爽很多。后面要加新条件,也不会到处翻 UI 代码。

get cols(): number {
if (this.selectedCount <= 1) return 1
if (this.selectedCount <= 4) return 2
return 3
}
片段 2:get currentFilter(): FilterPreset_wc1v {
这段代码承担的是页面里的“判断”或“动作”,把它从布局里抽出来后,build() 会清爽很多。后面要加新条件,也不会到处翻 UI 代码。
get currentFilter(): FilterPreset_wc1v {
return this.filterPresets[this.filterIndex]
}
使用方式
把下面代码放进 ArkTS 页面文件中即可运行。用于正式项目时,我会继续把数据模型、卡片 Builder 和页面事件拆出去,页面文件只负责组合。

建议练习顺序:先改状态字段 -> 再改交互事件 -> 最后调整 UI 样式
检查重点:点击是否刷新、列表 key 是否稳定、条件样式是否集中管理
这份代码可以继续扩展的方向
- 把模拟数据替换成接口数据
- 抽出复用卡片组件
- 增加空状态和异常状态
- 补充深色模式或主题色适配
- 对输入类场景增加校验提示
完整代码
// NineGridImageSplitPage - 图片九宫格切割 - 正方形裁剪预览与发布效果
interface GridCell {
row: number
col: number
bgX: number
bgY: number
}
interface FilterPreset_wc1v {
name: string
brightness: number
contrast: number
saturate: number
grayscale: number
}
@Entry
@Component
struct NineGridImageSplitPage {
@State selectedCount: number = 9
@State filterIndex: number = 0
@State previewMode: boolean = false
@State spacing: number = 4
private filterPresets: 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 },
]
private colorPalette: string[] = [
'#FF6B9D', '#C44569', '#F8B500', '#6C5CE7',
'#00B894', '#0984E3', '#E17055', '#74B9FF',
'#A29BFE',
]
get cols(): number {
if (this.selectedCount <= 1) return 1
if (this.selectedCount <= 4) return 2
return 3
}
get currentFilter(): FilterPreset_wc1v {
return this.filterPresets[this.filterIndex]
}
getCells(): GridCell[] {
const cells: GridCell[] = []
for (let r = 0; r < this.cols; r++) {
for (let c = 0; c < this.cols; c++) {
if (cells.length < this.selectedCount) {
cells.push({ row: r, col: c, bgX: c, bgY: r })
}
}
}
return cells
}
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 更稳。
更多推荐


所有评论(0)