当前位置: 首页 > news >正文

# 简易画板深度解析:Grid 像素格布局、画图/擦除模式切换与颜色预设

一、引言

画板是用户创意表达的工具。虽然 ArkTS 原生支持Canvas组件用于自由绘图,但本应用采用了一种更轻量、更声明式的方式——使用Grid网格布局模拟 10×10 像素画板,每个格子是一个独立的GridItem

从技术角度来看,这个应用覆盖了 ArkTS 中几个独特的模式:

  1. Grid 的精确网格控制columnsTemplate('1fr × 10')创建 10×10 的规整像素网格
  2. 状态驱动的像素着色:每个格子的颜色由数组状态管理,点击即改变
  3. 模式切换(画图/擦除):通过Button的选中态装饰器切换两种交互模式
  4. 颜色预设选择:7 种预设颜色,选中态高亮显示
  5. 模拟像素艺术:在无 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,而不是"格子被涂色时的颜色"。这意味着:

  1. 用户用蓝色画了几个格子(grid[0]=1, grid[1]=1
  2. 用户切换到红色(currentColor = '#FF3B30'
  3. 之前蓝色的格子变成了红色——因为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精确控制

columnsTemplateGrid组件最强大的布局属性:

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 更新,性能开销巨大。整体替换一次触发一次更新,效率极高。


六、扩展方向

  1. 支持 Canvas 自由绘制:集成Canvas组件,支持手指拖拽连续绘制而非逐格点击
  2. 填充工具:实现"油漆桶"功能,点击后填充相邻同色区域(Flood Fill 算法)
  3. 保存与分享:将像素画板内容导出为图片,通过分享 Kit 分享
  4. 撤销/重做:保存操作栈(Command Pattern),支持 Ctrl+Z 撤销
  5. 多种笔刷大小:支持 1×1、2×2、3×3 笔刷
  6. 对称绘制:水平/垂直对称模式,画一笔出现对称的两笔
  7. 导入模板:加载预设图案模板(爱心、笑脸、星星等)

七、本章小结

知识点掌握程度
一维数组映射二维网格✅ 理解Math.floor(i/w)+i%w映射
columnsTemplate('1fr ...')✅ 掌握 Grid 精确列控制
画图/擦除模式切换✅ 掌握互斥按钮的设计模式
颜色预设选中态高亮✅ 理解条件边框样式
数组整体替换 vs 逐元素赋值✅ 理解性能差异
new Array(100).fill(0)✅ 掌握固定大小数组的创建

简易画板展示了 ArkTS 中用数据驱动网格 UI的经典模式,以及模式切换的设计思想。虽然在真正的绘图应用中 Canvas 更为合适,但 Grid 方式在学习数组状态管理和网格布局方面有独特的教学价值。

http://www.cnnetsun.cn/news/3245515.html

相关文章:

  • B2B订货系统开发服务商推荐:2026年最新测评
  • 数据流图(DFD) 3大平衡原则实战:从软考真题解析到5步排错法
  • 2026预算有限建站工具哪家好
  • OpenGL 4.6 渲染管线 4 大阶段详解:从 Vertex Shader 到 Framebuffer 的完整数据流
  • Python开发的下载神器!Ghost Downloader:多线程+断点续传,像IDM一样快还不用合并文件!
  • VMware虚拟机启用Windows 7 Aero特效的硬件要求与配置指南
  • 端到端成趋势,决策范式革新
  • Claude API 200K 上下文窗口实测:长文本输入、JSON 抽取、RAG 选型与工程避坑
  • Xilinx MIG 7 Series IP 配置实战:DDR3 64bit 位宽优化与 200MHz UI 时钟生成
  • 2026设备售后管理系统选型指南:破解工单、备件、现场服务三大难题
  • CC-Switch:面向生产级Agent的多模型多协议中央治理平台
  • 商用短信服务商评估手册:垂类平台与云厂商优劣势拆解
  • LeetCode238:乘积除自身的高效解法
  • 流感病毒治疗靶标HA
  • I2C与SPI协议深度对比:从4个维度实测STM32驱动OLED屏性能
  • 基于TB6593FNG与PIC32的直流电机控制系统设计
  • Day18 | Agentic RAG——让模型自己决定查不查、查什么、查几次
  • TDA7468与PIC18F56K42构建高效音频处理系统
  • 【小白也能轻松玩转龙虾】OpenClaw v2.7.9 虾壳云一键部署 (附最新安装包)
  • 超低功耗设计:NBM7100A与STM32F723ZE的电源管理优化
  • C++协议分析器实战:从抓包到解码的网络流量解析
  • 74LS112 JK 触发器设计 4 位同步计数器:Quartus II 仿真与毛刺分析
  • 如何快速部署Postman便携版:Windows系统免安装API测试终极指南
  • RT-Thread 5.0.2 DAC 驱动框架解析:HAL 库对接与 2 种使用模式对比
  • d2s-editor深度解析:揭秘暗黑2存档编辑器的二进制魔法与可视化哲学
  • 短距离隔墙遥控总失灵?从射频底层拆解433MHz Sub-GHz的穿墙技术逻辑
  • 零工配送从业者定向网络钓鱼攻击机理与多层智能防御体系研究
  • 直流负载管理优化:继电器选型与MCU控制实践
  • 如何快速定制专属机械键盘:Cherry MX键帽3D打印完整指南 [特殊字符]
  • 单调栈、单调队列(模板)、子矩阵(模板)