一、引言
画板是用户创意表达的工具。虽然 ArkTS 原生支持Canvas组件用于自由绘图,但本应用采用了一种更轻量、更声明式的方式——使用Grid网格布局模拟 10×10 像素画板,每个格子是一个独立的GridItem。
从技术角度来看,这个应用覆盖了 ArkTS 中几个独特的模式:
- Grid 的精确网格控制:
columnsTemplate('1fr × 10')创建 10×10 的规整像素网格 - 状态驱动的像素着色:每个格子的颜色由数组状态管理,点击即改变
- 模式切换(画图/擦除):通过
Button的选中态装饰器切换两种交互模式 - 颜色预设选择:7 种预设颜色,选中态高亮显示
- 模拟像素艺术:在无 Canvas 的情况下实现类像素画的交互体验
本文将从网格布局的数学原理出发,深入到 ArkTS 中用数组驱动 UI 渲染的核心思想。
二、完整源码
// index16.ets@Entry@Componentstruct Index16{@Stategrid:number[]=newArray(100).fill(0);@StatecurrentColor:string='#007AFF';@StateisEraseMode:boolean=false;privatecolors:string[]=['#FF3B30','#FF9500','#FFCC00','#34C759','#007AFF','#AF52DE','#000000'];paintCell(index:number):void{this.grid[index]=this.isEraseMode?0:1;}clearAll():void{this.grid=newArray(100).fill(0);}getCellColor(value:number):string{returnvalue===1?this.currentColor:'#F0F0F0';}build(){Column(){Text('🎨 简易画板').fontSize(28).fontWeight(FontWeight.Bold).margin({top:40,bottom:5})Text('点击格子涂色,10×10 像素画板').fontSize(13).fontColor('#8E8E93').margin({bottom:15})// 颜色预设Row({space:6}){ForEach(this.colors,(color:string)=>{Column().width(28).height(28).backgroundColor(color).borderRadius(14).border({width:this.currentColor===color&&!this.isEraseMode?3:1,color:this.currentColor===color&&!this.isEraseMode?'#333333':'#DDDDDD'}).onClick(()=>{this.currentColor=color;this.isEraseMode=false;})},(color:string)=>color)}.margin({bottom:10})// 模式切换Row({space:12}){Button('✏️ 画图').height(36).fontSize(13).backgroundColor(this.isEraseMode?'#E5E5EA':'#007AFF').fontColor(this.isEraseMode?'#333333':'#FFFFFF').borderRadius(18).onClick(()=>{this.isEraseMode=false;})Button('🧹 擦除').height(36).fontSize(13).backgroundColor(this.isEraseMode?'#FF3B30':'#E5E5EA').fontColor(this.isEraseMode?'#FFFFFF':'#333333').borderRadius(18).onClick(()=>{this.isEraseMode=true;})Button('🗑 清空').height(36).fontSize(13).backgroundColor('#8E8E93').fontColor('#FFFFFF').borderRadius(18).onClick(()=>{this.clearAll();})}.margin({bottom:15})// 10×10 像素网格Grid(){ForEach(this.grid,(value:number,index:number)=>{GridItem(){Column().width('100%').height('100%').backgroundColor(this.getCellColor(value)).border({width:1,color:'#E0E0E0'}).onClick(()=>{this.paintCell(index);})}},(value:number,index:number)=>index.toString())}.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr').rowsGap(0).columnsGap(0).width(280).height(280)Text('提示: 点击格子填充颜色,支持画图/擦除模式切换').fontSize(12).fontColor('#AAAAAA').margin({top:20})}.width('100%').height('100%').justifyContent(FlexAlign.Start).alignItems(HorizontalAlign.Center)}}三、核心技术解析
3.1 数组状态驱动 UI:100 个像素的数据结构
画板的核心数据是一个长度为 100 的数字数组:
@Stategrid:number[]=newArray(100).fill(0);每个元素代表一个像素格子的状态:
0:空白(默认颜色#F0F0F0)1:已着色(使用当前选中的颜色)
new Array(100).fill(0)一次性创建 100 个元素并初始化为 0。这是 JavaScript 创建固定大小数组并填充默认值的最简洁方式。
数组索引 → 屏幕坐标的映射:
索引 i → 行 = Math.floor(i / 10) 列 = i % 10 例如 i = 23: 行 = Math.floor(23 / 10) = 2 列 = 23 % 10 = 3 → 第 3 行第 4 列(从 0 开始编号)这种一维数组映射二维网格的技术在游戏开发(如贪吃蛇、2048、扫雷)中广泛使用。
单元格颜色读取函数:
getCellColor(value:number):string{returnvalue===1?this.currentColor:'#F0F0F0';}这个函数的精妙之处在于:当value = 1时,它返回的是当前的currentColor。这意味着如果用户先画了一些蓝色格子,然后切换颜色为红色,之前画的蓝色格子仍然是蓝色(因为grid[i]的值保持为 1,而getCellColor返回的是当前的currentColor……等等,这其实是一个 bug!)
发现问题了吗?当value = 1时,getCellColor总是返回当前的currentColor,而不是"格子被涂色时的颜色"。这意味着:
- 用户用蓝色画了几个格子(
grid[0]=1, grid[1]=1) - 用户切换到红色(
currentColor = '#FF3B30') - 之前蓝色的格子变成了红色——因为
getCellColor(1)总是返回this.currentColor
修复方案:将grid改为存储具体的颜色值,而非简单的 0/1 标记:
@Stategrid:string[]=newArray(100).fill('');paintCell(index:number):void{this.grid[index]=this.isEraseMode?'':this.currentColor;}getCellColor(value:string):string{returnvalue||'#F0F0F0';// 空字符串 → 底色,#XXXXXX → 该格子的颜色}这样每个格子独立存储自己的颜色值,切换颜色后不会影响已画好的格子。这是原始代码的一个设计缺陷,也是从Demo 代码到生产级代码需要跨越的鸿沟。
3.2 Grid 的columnsTemplate精确控制
columnsTemplate是Grid组件最强大的布局属性:
Grid().columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr').rowsGap(0).columnsGap(0)columnsTemplate的语法类似 CSS Grid 的grid-template-columns:
| 语法 | 含义 | 示例 |
|---|---|---|
1fr | 比例单位(fraction) | 3 个1fr= 三等分 |
px | 固定像素 | 50px= 固定 50 像素 |
% | 百分比 | 20%= 父容器宽度的 20% |
repeat(n, 1fr) | 重复 | 部分版本支持 |
'1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr'表示 10 列等宽的网格。当 Grid 总宽度为 280px 时,每列宽度为 28px,每个格子是一个 28×28 的正方形。
rowsGap(0)和columnsGap(0)将间距设为 0,格子之间仅通过 1px 的border分隔。使用border而非gap的原因在于:边框在每个格子自己的区域内,不影响整体布局的精确计算。
3.3 画图/擦除模式切换
画板支持两种交互模式,通过布尔值isEraseMode控制:
@StateisEraseMode:boolean=false;paintCell(index:number):void{this.grid[index]=this.isEraseMode?0:1;}模式切换的 UI 表现:
画图模式:isEraseMode = false
- "画图"按钮:蓝色(#007AFF),白色文字——表示当前激活
- "擦除"按钮:灰色(#E5E5EA),深色文字——表示非激活
擦除模式:isEraseMode = true
- "画图"按钮:灰色,深色文字
- "擦除"按钮:红色(#FF3B30),白色文字——表示当前激活
Button('✏️ 画图').backgroundColor(this.isEraseMode?'#E5E5EA':'#007AFF').fontColor(this.isEraseMode?'#333333':'#FFFFFF')Button('🧹 擦除').backgroundColor(this.isEraseMode?'#FF3B30':'#E5E5EA').fontColor(this.isEraseMode?'#FFFFFF':'#333333')这是互斥按钮组的典型实现方式——用一个状态变量控制两个按钮的样式。
3.4 颜色预设选择
画板提供了 7 种预设颜色,使用ForEach渲染:
privatecolors:string[]=['#FF3B30','#FF9500','#FFCC00','#34C759','#007AFF','#AF52DE','#000000'];颜色预设的选中态通过边框宽度和颜色来实现:
Column().width(28).height(28).backgroundColor(color).borderRadius(14).border({width:this.currentColor===color&&!this.isEraseMode?3:1,color:this.currentColor===color&&!this.isEraseMode?'#333333':'#DDDDDD'})当某个颜色是当前选中色且不在擦除模式时,该颜色圆点显示 3px 深色边框;否则显示 1px 浅色边框。
&&条件:this.currentColor === color && !this.isEraseMode确保当用户切换到擦除模式时,所有颜色预设都不显示"选中"状态——因为擦除模式下选色是无效的。
四、Grid 组件与其他布局的对比
在 ArkTS 中创建网格布局有多种方式:
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
Grid+columnsTemplate | 精确控制行列,自动换行 | 需要指定列数 | 像素画板、九宫格、图片墙 |
Column+ 多个Row | 每行可不同布局 | 手动管理行数 | 计算器按键、表单 |
Flexwrap | 自动换行 | 控制不够精细 | 标签、徽章 |
对于画板这种规整的网格(每行每列数量固定、格子大小完全一致),Grid是最佳选择。
五、清空操作:数组替换 vs 逐元素赋值
clearAll():void{this.grid=newArray(100).fill(0);}这里通过整体替换数组(而不是for循环逐个赋值)来实现清空。这是有意为之的:
整体替换:
this.grid=newArray(100).fill(0);// ✅ 触发 @State 更新逐个赋值(不可行):
for(leti=0;i<100;i++){this.grid[i]=0;// ⚠️ 100次触发更新,性能极差}// 或者this.grid.fill(0);// ⚠️ 部分 ArkTS 版本不支持ArkTS 的@State对数组的代理支持直接索引赋值(this.grid[i] = x),但 100 次for循环赋值会导致 100 次 UI 更新,性能开销巨大。整体替换一次触发一次更新,效率极高。
六、扩展方向
- 支持 Canvas 自由绘制:集成
Canvas组件,支持手指拖拽连续绘制而非逐格点击 - 填充工具:实现"油漆桶"功能,点击后填充相邻同色区域(Flood Fill 算法)
- 保存与分享:将像素画板内容导出为图片,通过分享 Kit 分享
- 撤销/重做:保存操作栈(
Command Pattern),支持 Ctrl+Z 撤销 - 多种笔刷大小:支持 1×1、2×2、3×3 笔刷
- 对称绘制:水平/垂直对称模式,画一笔出现对称的两笔
- 导入模板:加载预设图案模板(爱心、笑脸、星星等)
七、本章小结
| 知识点 | 掌握程度 |
|---|---|
| 一维数组映射二维网格 | ✅ 理解Math.floor(i/w)+i%w映射 |
columnsTemplate('1fr ...') | ✅ 掌握 Grid 精确列控制 |
| 画图/擦除模式切换 | ✅ 掌握互斥按钮的设计模式 |
| 颜色预设选中态高亮 | ✅ 理解条件边框样式 |
| 数组整体替换 vs 逐元素赋值 | ✅ 理解性能差异 |
new Array(100).fill(0) | ✅ 掌握固定大小数组的创建 |
简易画板展示了 ArkTS 中用数据驱动网格 UI的经典模式,以及模式切换的设计思想。虽然在真正的绘图应用中 Canvas 更为合适,但 Grid 方式在学习数组状态管理和网格布局方面有独特的教学价值。