在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、概述

在HarmonyOS应用开发中,UI布局与样式设计是构建优秀用户体验的核心环节。本项目基于ArkTS技术栈,采用深蓝色背景配金色主题的视觉风格,通过精心设计的布局结构和样式系统,打造了一套统一、美观、交互流畅的游戏界面。

本文将深入探讨项目中使用的UI布局与样式设计技巧,包括主题配色方案、布局组件使用、响应式设计、组件复用、视觉效果等方面的实践经验。

1.1 设计原则

本项目的UI设计遵循以下核心原则:

  • 一致性:所有页面采用统一的配色方案、字体风格和交互模式
  • 清晰性:信息层次分明,操作路径简洁明了
  • 美观性:深蓝色背景配金色主题,营造专业、高端的视觉感受
  • 可用性:按钮尺寸适中,点击区域合理,操作反馈及时
  • 响应式:适配不同屏幕尺寸和网格大小

1.2 技术栈

  • 框架:HarmonyOS ArkUI(声明式UI)
  • 语言:ArkTS(TypeScript方言)
  • 布局:Flexbox布局系统(Column、Row)
  • 网格:Grid组件
  • 状态管理:@State、@Prop、@Link等状态装饰器

二、主题配色方案

2.1 核心配色

本项目采用深蓝色背景配金色主题的经典配色方案:

┌─────────────────────────────────────────────────────────────┐
│                     主题配色方案                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  背景色:  #0A192F (深蓝色)                                  │
│  主题色:  #FFD700 (金色)                                    │
│  辅助色:  #87CEEB (天蓝色)                                  │
│  成功色:  #32CD32 (绿色)                                    │
│  警告色:  #FF6B6B (红色)                                    │
│  文字色:  #FFFFFF (白色)                                    │
│  边框色:  rgba(255, 215, 0, 0.3) (金色半透明)               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.2 配色应用场景

颜色 用途 示例
#0A192F 页面背景、容器背景 所有页面的背景色
#FFD700 标题、按钮、高亮元素 页面标题、功能按钮、通关提示
#87CEEB 提示文字、次要信息 游戏说明、计时、分数显示
#32CD32 成功状态、正确答案 24点正确结果、数字输入确认
#FF6B6B 错误状态、警告提示 错误数字标记、擦除按钮
#FFFFFF 主要文字、数字显示 游戏中的数字、普通文本

2.3 配色代码示例

// 页面背景
.backgroundColor('#0A192F')

// 金色标题
Text('游戏标题')
  .fontColor('#FFD700')
  .fontWeight(FontWeight.Bold)

// 天蓝色辅助文字
Text('提示信息')
  .fontColor('#87CEEB')

// 金色按钮
Button('开始游戏')
  .backgroundColor('#FFD700')
  .fontColor('#0A192F')

// 金色边框
.borderColor('#FFD700')
.borderWidth(1)

2.4 透明度应用

项目中大量使用透明度来创建层次感:

// 金色半透明背景
.backgroundColor('rgba(255, 215, 0, 0.1)')

// 白色半透明背景
.backgroundColor('rgba(255, 255, 255, 0.05)')

// 选中状态高亮
.backgroundColor('rgba(255, 215, 0, 0.25)')

透明度设计原理

  1. 低透明度(0.05-0.1):用于背景元素,增加层次感但不干扰主要内容
  2. 中等透明度(0.2-0.3):用于选中状态、悬停效果,提供视觉反馈
  3. 高透明度(0.5以上):用于覆盖层、弹窗背景,突出前景内容

三、布局组件使用技巧

3.1 Column布局

Column组件用于垂直方向的元素排列,是页面布局的基础组件:

build() {
  Column({ space: 16 }) {
    // 顶部标题区域
    Row() {
      Text('游戏标题')
        .fontSize(24)
        .fontColor('#FFD700')
    }
    .width('100%')
    .padding({ top: 40 })

    // 游戏内容区域
    Column() {
      // 游戏主体内容
    }
    .flexGrow(1)

    // 底部按钮区域
    Row() {
      Button('按钮1')
      Button('按钮2')
    }
    .width('100%')
    .padding({ bottom: 60 })
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#0A192F')
}

Column布局要点

  1. space属性:设置子元素之间的间距
  2. flexGrow:设置子元素的弹性增长系数
  3. width/height:设置容器尺寸,通常设置为’100%'占满父容器
  4. padding:设置内边距,避免内容贴边

3.2 Row布局

Row组件用于水平方向的元素排列:

Row({ space: 12 }) {
  // 返回按钮
  Button('← 返回')
    .fontSize(14)
    .fontColor('#FFD700')
    .backgroundColor('transparent')
    .borderColor('#FFD700')
    .borderWidth(1)

  // 标题
  Text('游戏标题')
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
    .fontColor('#FFD700')

  // 右侧信息
  Text('分数: 100')
    .fontSize(12)
    .fontColor('#87CEEB')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 20, right: 20 })

Row布局要点

  1. justifyContent:设置水平对齐方式

    • FlexAlign.Start:左对齐
    • FlexAlign.Center:居中对齐
    • FlexAlign.End:右对齐
    • FlexAlign.SpaceBetween:两端对齐
    • FlexAlign.SpaceAround:均匀分布
  2. alignItems:设置垂直对齐方式

    • HorizontalAlign.Start:顶部对齐
    • HorizontalAlign.Center:垂直居中
    • HorizontalAlign.End:底部对齐

3.3 Grid布局

Grid组件用于网格布局,是游戏界面中最常用的布局组件:

Grid() {
  ForEach(this.gridData, (item: number, index: number) => {
    GridItem() {
      Text(item.toString())
        .fontSize(20)
        .fontColor('#FFFFFF')
    }
    .backgroundColor('rgba(255, 215, 0, 0.1)')
    .borderColor('#FFD700')
    .borderWidth(1)
    .borderRadius(8)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  })
}
.width('90%')
.height(300)
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr')

Grid布局要点

  1. columnsTemplate:设置列模板,使用fr单位表示等分
  2. rowsTemplate:设置行模板
  3. GridItem:网格项组件
  4. aspectRatio:设置宽高比,保持网格正方形

3.4 Scroll布局

Scroll组件用于可滚动区域,适用于内容超出屏幕的情况:

Scroll() {
  Column({ space: 16 }) {
    ForEach(this.items, (item: any) => {
      // 列表项
    })
  }
  .padding({ top: 16, bottom: 200 })
}
.width('100%')
.flexGrow(1)
.backgroundColor('#0A192F')

Scroll布局要点

  1. flexGrow:设置为1,占满剩余空间
  2. bottom padding:设置较大的底部内边距,避免内容被导航栏遮挡
  3. 背景色:通常与外层容器一致

3.5 布局嵌套技巧

复杂页面需要多层布局嵌套,以下是常见的嵌套模式:

// 三层嵌套:Column → Row → Column
Column() {
  Row() {
    Column() {
      Text('图标')
      Text('名称')
    }
    .flexGrow(1)
    
    Text('箭头')
  }
  .width('100%')
}

嵌套原则

  1. 避免过深嵌套:尽量控制在3-4层以内
  2. 语义化布局:根据内容语义选择合适的布局组件
  3. 合理使用flexGrow:控制子元素的空间分配
  4. 保持一致性:同类页面使用相同的布局结构

四、响应式设计技巧

4.1 动态字体大小

根据网格尺寸调整字体大小,确保数字在不同网格中都能清晰显示:

Text(value.toString())
  .fontSize(this.boardSize === 4 ? 28 : (this.boardSize === 6 ? 22 : 18))

字体大小策略

网格尺寸 字体大小 说明
4×4 28sp 格子较大,可使用较大字体
6×6 22sp 格子适中,中等字体
9×9 18sp 格子较小,使用较小字体

4.2 动态网格高度

根据网格尺寸调整数字键盘的高度:

.height(this.boardSize === 9 ? 160 : 100)

高度策略

  • 9×9网格需要更多空间显示数字键盘,设置为160px
  • 4×4和6×6网格使用较小的键盘高度100px

4.3 动态列数

根据网格尺寸调整数字键盘的列数:

.columnsTemplate(this.boardSize === 4 ? '1fr 1fr' : '1fr 1fr 1fr')

列数策略

  • 4×4网格:2列×2行布局
  • 6×6网格:3列×2行布局
  • 9×9网格:3列×3行布局

4.4 动态边框计算

数独游戏中根据宫格边界动态计算边框宽度和颜色:

private getBorderWidth(index: number): BorderWidth {
  let col: number = index % this.boardSize;
  let row: number = Math.floor(index / this.boardSize);
  let boxRowSize: number = this.getBoxRowSize();
  let boxColSize: number = this.getBoxColSize();
  let right: number = (col % boxColSize === boxColSize - 1) ? 2 : 1;
  let bottom: number = (row % boxRowSize === boxRowSize - 1) ? 2 : 1;
  return new BorderWidth(1, 1, right, bottom);
}

动态边框原理

  • 使用取模运算判断格子是否位于宫格边界
  • 宫格边界使用较粗的边框(2px)
  • 普通格子边界使用较细的边框(1px)

五、组件设计技巧

5.1 按钮设计

按钮是游戏中最常用的交互组件,本项目采用统一的按钮设计风格:

// 主要按钮(金色背景)
Button('开始游戏')
  .fontSize(18)
  .fontColor('#0A192F')
  .backgroundColor('#FFD700')
  .borderRadius(12)
  .padding({ left: 40, right: 40, top: 12, bottom: 12 })

// 次要按钮(透明背景+金色边框)
Button('← 返回')
  .fontSize(14)
  .fontColor('#FFD700')
  .backgroundColor('transparent')
  .borderColor('#FFD700')
  .borderWidth(1)

// 危险按钮(红色主题)
Button('❌ 擦除')
  .fontSize(14)
  .fontColor('#FF6B6B')
  .backgroundColor('rgba(255, 107, 107, 0.2)')
  .borderColor('#FF6B6B')
  .borderWidth(1)

按钮设计规范

类型 背景色 文字色 边框 圆角
主要按钮 #FFD700 #0A192F 12px
次要按钮 透明 #FFD700 金色1px 8px
危险按钮 红色透明 #FF6B6B 红色1px 8px

5.2 卡片设计

卡片组件用于展示游戏列表、关卡信息等内容:

Button() {
  Row({ space: 16 }) {
    // 图标区域
    Column() {
      Text(category.icon)
        .fontSize(32)
    }
    .width(60)
    .height(60)
    .backgroundColor('rgba(255, 215, 0, 0.1)')
    .borderColor(category.color)
    .borderWidth(1)
    .borderRadius(12)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)

    // 内容区域
    Column({ space: 4 }) {
      Row({ space: 8 }) {
        Text(category.name)
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .fontColor('#FFFFFF')
        Text('入门')
          .fontSize(10)
          .fontColor('#32CD32')
          .backgroundColor('rgba(255, 255, 255, 0.1)')
          .borderRadius(10)
          .padding({ left: 6, right: 6 })
      }
      Text(category.description)
        .fontSize(12)
        .fontColor('#87CEEB')
    }
    .flexGrow(1)
  }
  .width('100%')
}
.width('90%')
.height(90)
.padding({ left: 8, right: 8 })
.backgroundColor('rgba(255, 255, 255, 0.05)')
.borderColor('rgba(255, 215, 0, 0.3)')
.borderWidth(1)
.borderRadius(16)

卡片设计规范

  • 背景色:rgba(255, 255, 255, 0.05)(白色极淡透明)
  • 边框:金色半透明1px
  • 圆角:16px
  • 内边距:左右8px
  • 高度:90px

5.3 数字键盘设计

数字键盘组件用于数独、数字炸弹等游戏的数字输入:

Grid() {
  ForEach(this.getNumberList(), (num: number) => {
    GridItem() {
      Button(num.toString())
        .fontSize(this.boardSize === 4 ? 24 : (this.boardSize === 6 ? 20 : 16))
        .fontColor('#FFD700')
        .backgroundColor('rgba(255, 215, 0, 0.1)')
        .borderColor('#FFD700')
        .borderWidth(1)
        .borderRadius(8)
        .width('100%')
        .height('100%')
        .onClick(() => this.inputNumber(num))
    }
  })
}
.width('90%')
.height(this.boardSize === 9 ? 160 : 100)
.columnsTemplate(this.boardSize === 4 ? '1fr 1fr' : '1fr 1fr 1fr')
.rowsTemplate(this.boardSize === 4 ? '1fr 1fr' : (this.boardSize === 6 ? '1fr 1fr' : '1fr 1fr 1fr'))

数字键盘设计要点

  1. 动态字体大小:根据网格尺寸调整
  2. 金色主题:金色文字和边框
  3. 圆角设计:8px圆角
  4. 响应式布局:根据数字数量调整行列数

5.4 功能按钮组设计

功能按钮组用于组织游戏中的操作按钮:

Row({ space: 16 }) {
  Button('❌ 擦除')
    .fontSize(14)
    .fontColor('#FF6B6B')
    .backgroundColor('rgba(255, 107, 107, 0.2)')
    .borderColor('#FF6B6B')
    .borderWidth(1)
    .borderRadius(8)
    .flexGrow(1)
    .height(40)

  Button('↪ 重做')
    .fontSize(14)
    .fontColor('#FFD700')
    .backgroundColor('transparent')
    .borderColor('#FFD700')
    .borderWidth(1)
    .borderRadius(8)
    .flexGrow(1)
    .height(40)
}
.width('90%')
.padding({ top: 8 })

功能按钮组设计要点

  1. flexGrow:按钮均分宽度
  2. 固定高度:40px
  3. 间距:16px
  4. 图标+文字:使用emoji图标增强视觉识别

六、视觉效果设计

6.1 边框设计

边框是UI设计中重要的视觉元素,本项目使用多种边框样式:

// 普通边框
.borderColor('#FFD700')
.borderWidth(1)

// 宫格边框(较粗)
.borderWidth(2)
.borderColor('#FFD700')

// 半透明边框
.borderColor('rgba(255, 215, 0, 0.3)')
.borderWidth(1)

// 错误边框
.borderColor('#FF6B6B')
.borderWidth(1)

边框设计原则

  1. 统一颜色:主要使用金色边框
  2. 粗细对比:重要边界使用较粗边框(2px),普通边界使用较细边框(1px)
  3. 透明度:非关键边框使用半透明效果

6.2 圆角设计

圆角设计使界面更加柔和、现代:

// 大圆角(卡片、按钮)
.borderRadius(16)

// 中等圆角(数字键盘、功能按钮)
.borderRadius(12)

// 小圆角(单元格)
.borderRadius(8)

// 圆形(头像、图标)
.borderRadius(50)

圆角设计规范

组件类型 圆角大小 说明
卡片 16px 较大圆角,柔和感强
主要按钮 12px 中等圆角,现代感
功能按钮 8px 小圆角,精致感
单元格 8px 小圆角,统一风格

6.3 阴影设计

阴影用于增加层次感和深度:

.shadow({ radius: 2, color: '#00000008', offsetY: 1 })

阴影参数说明

  • radius:阴影模糊半径
  • color:阴影颜色(带透明度)
  • offsetX:水平偏移
  • offsetY:垂直偏移

阴影使用场景

  1. 卡片悬浮效果:轻微阴影增加层次感
  2. 按钮点击效果:按下时阴影加深
  3. 弹窗效果:较深阴影突出弹窗

6.4 渐变背景

虽然本项目主要使用纯色背景,但渐变背景也是常见的视觉效果:

// 线性渐变
.linearGradient({
  direction: GradientDirection.Bottom,
  colors: ['#0A192F', '#112240']
})

// 径向渐变
.radialGradient({
  center: { x: 0.5, y: 0.5 },
  radius: 1,
  colors: ['#FFD700', '#0A192F']
})

注意事项

  • 当前SDK版本中LinearGradient存在枚举值兼容性问题
  • 建议使用纯色背景替代渐变,或确保SDK版本支持

七、状态驱动的UI更新

7.1 @State装饰器

@State用于组件内部状态管理,状态变化会自动触发UI更新:

@State isPlaying: boolean = false;
@State score: number = 0;
@State timerSeconds: number = 0;

@State使用要点

  1. 声明位置:在组件类中声明
  2. 初始值:必须提供初始值
  3. 响应式更新:状态变化自动触发UI重渲染

7.2 数组状态更新

ArkTS中数组的直接修改不会触发UI更新,必须创建新数组引用:

// 错误方式:直接修改数组元素
this.cellValues[index] = num;

// 正确方式:创建新数组触发更新
this.cellValues[index] = num;
this.cellValues = this.cellValues.slice();

// 或者使用push后重新赋值
completedLevels.push(this.currentLevel);
appState.completedLevels = completedLevels;

数组更新策略

  1. slice()方法:创建数组副本,触发响应式更新
  2. 重新赋值:将修改后的数组重新赋值给状态变量
  3. 避免直接修改:不要直接修改数组元素而不重新赋值

7.3 条件渲染

根据状态条件显示不同的UI内容:

if (!this.isPlaying && !this.isGameWon) {
  // 未开始游戏状态
  Button('开始游戏')
    .onClick(() => this.startGame())
} else if (this.isGameWon) {
  // 通关状态
  Column() {
    Text('🎉 恭喜通关!')
      .fontSize(24)
      .fontColor('#FFD700')
    Button('下一关')
      .onClick(() => router.back())
  }
} else {
  // 游戏进行中状态
  Column() {
    // 游戏内容
  }
}

条件渲染要点

  1. 状态判断:使用if-else进行状态判断
  2. 互斥状态:确保状态之间互斥,避免同时显示多个状态的UI
  3. 性能优化:条件渲染避免不必要的组件渲染

7.4 ForEach循环渲染

ForEach用于动态渲染列表数据:

ForEach(this.categories, (category: Category) => {
  Button() {
    Text(category.name)
      .fontColor('#FFFFFF')
  }
  .backgroundColor('rgba(255, 255, 255, 0.05)')
  .onClick(() => this.startGame(category))
})

ForEach使用要点

  1. 数据源:第一个参数是数组数据源
  2. 迭代器:第二个参数是迭代函数,返回单个元素的UI
  3. key参数:可选的第三个参数用于指定唯一标识,优化渲染性能

八、布局性能优化

8.1 避免过度渲染

过度渲染会导致页面卡顿,以下是一些优化技巧:

// 避免:在循环中创建复杂组件
ForEach(items, (item) => {
  Column() {
    // 复杂的嵌套组件
  }
})

// 优化:提取为独立组件
ForEach(items, (item) => {
  MyItemComponent({ data: item })
})

优化策略

  1. 组件拆分:将复杂的列表项提取为独立组件
  2. 条件渲染:只渲染当前需要的内容
  3. 懒加载:对于大量数据,采用分页或懒加载

8.2 合理使用flexGrow

flexGrow控制子元素的空间分配,合理使用可以避免布局溢出:

Column() {
  Row() {
    // 固定高度的导航栏
  }
  .height(60)

  Scroll() {
    // 可滚动内容区域
  }
  .flexGrow(1)  // 占满剩余空间

  Row() {
    // 固定高度的底部按钮
  }
  .height(80)
}
.height('100%')

flexGrow使用原则

  1. 唯一flexGrow:在垂直布局中,通常只有一个元素设置flexGrow为1
  2. 固定高度:顶部和底部元素设置固定高度
  3. 避免多重flexGrow:多个元素设置flexGrow会导致空间分配复杂

8.3 Grid组件优化

Grid组件在处理大量数据时需要注意性能:

// 避免:Grid没有设置固定高度
Grid() {
  ForEach(largeData, (item) => {
    GridItem() { /* ... */ }
  })
}
.width('100%')  // 没有设置高度

// 优化:设置固定高度或使用aspectRatio
Grid() {
  ForEach(largeData, (item) => {
    GridItem() { /* ... */ }
  })
}
.width('100%')
.aspectRatio(1)  // 保持正方形

Grid优化策略

  1. 设置高度:使用固定高度或aspectRatio
  2. 限制数据量:避免一次渲染过多数据
  3. 使用虚拟化:如果支持,使用虚拟化列表

8.4 避免布局抖动

布局抖动是指频繁的布局计算导致的性能问题:

// 避免:频繁修改布局属性
Button('点击')
  .onClick(() => {
    this.width = this.width + 10;
    this.height = this.height + 10;
  })

// 优化:使用transform代替布局属性修改
Button('点击')
  .onClick(() => {
    this.scale = this.scale + 0.1;
  })

布局抖动优化

  1. 使用transform:缩放、旋转使用transform属性
  2. 批量更新:合并多个布局属性的修改
  3. 避免动画中修改布局:动画过程中避免修改width、height等属性

九、跨页面样式统一

9.1 全局样式定义

虽然ArkTS没有CSS全局样式,但可以通过以下方式实现样式统一:

// 定义样式常量
const PRIMARY_COLOR = '#FFD700';
const BACKGROUND_COLOR = '#0A192F';
const ACCENT_COLOR = '#87CEEB';

// 使用常量
.backgroundColor(BACKGROUND_COLOR)
.fontColor(PRIMARY_COLOR)

样式常量管理

  1. 集中定义:在单独的文件中定义所有样式常量
  2. 统一引用:所有组件引用同一个常量文件
  3. 易于维护:修改样式只需更新常量文件

9.2 页面布局模板

所有游戏页面使用统一的布局模板:

build() {
  Column() {
    // 顶部导航栏
    Row() {
      Button('← 返回')
      Text('游戏标题')
      Text('分数/时间')
    }
    .width('100%')
    .padding({ left: 20, right: 20 })
    .margin({ top: 40 })

    // 游戏内容区域
    Column() {
      // 具体游戏内容
    }
    .flexGrow(1)

    // 底部功能区
    Column() {
      // 功能按钮、键盘等
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#0A192F')
  .padding({ bottom: 60 })
}

布局模板特点

  1. 统一结构:所有页面使用相同的三层结构
  2. 标准化导航:顶部导航栏包含返回按钮、标题、信息显示
  3. 固定底部:底部保留60px内边距,避免内容被遮挡

9.3 组件复用

提取通用组件,避免重复代码:

// 通用计时器组件
@Component
struct Timer {
  @State seconds: number = 0;
  
  build() {
    Text(this.formatTime(this.seconds))
      .fontSize(24)
      .fontColor('#87CEEB')
  }
  
  private formatTime(seconds: number): string {
    let mins = Math.floor(seconds / 60);
    let secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  }
}

// 在多个页面中复用
Timer()

组件复用策略

  1. 提取通用功能:计时器、数字键盘、按钮组等
  2. 使用@Prop/@Link:通过属性传递数据
  3. 统一接口:相同类型的组件使用一致的接口

十、无障碍设计考虑

10.1 文字对比度

确保文字与背景有足够的对比度:

// 良好的对比度
Text('游戏标题')
  .fontColor('#FFD700')  // 金色
.backgroundColor('#0A192F')  // 深蓝色

// 良好的对比度
Text('提示信息')
  .fontColor('#87CEEB')  // 天蓝色
.backgroundColor('#0A192F')  // 深蓝色

// 避免:低对比度组合
Text('文字')
  .fontColor('#888888')  // 灰色
.backgroundColor('#999999')  // 浅灰色

对比度标准

  • 正常文字:至少4.5:1的对比度
  • 大文字(18pt以上):至少3:1的对比度

10.2 按钮尺寸

确保按钮有足够的点击区域:

// 推荐的按钮尺寸
Button('按钮')
  .width(80)
  .height(40)  // 最小40px高度

// 游戏功能按钮
Button('❌ 擦除')
  .flexGrow(1)
  .height(40)  // 固定40px高度

按钮尺寸规范

  • 最小触摸目标:48×48px
  • 功能按钮:至少40px高度
  • 间距:按钮之间至少8px间距

10.3 焦点管理

确保键盘导航和屏幕阅读器可以正确访问所有元素:

Button('开始游戏')
  .focusable(true)
  .tabIndex(1)

Button('设置')
  .focusable(true)
  .tabIndex(2)

焦点管理要点

  1. focusable:设置为true使元素可聚焦
  2. tabIndex:设置Tab键导航顺序
  3. 焦点样式:为聚焦状态设置明显的视觉反馈

十一、调试技巧

11.1 使用预览工具

HarmonyOS Studio提供了预览工具,可以实时查看布局效果:

  1. 实时预览:修改代码后立即查看效果
  2. 多设备预览:查看不同屏幕尺寸的效果
  3. 布局检查:检查组件的尺寸和位置

11.2 添加调试日志

在关键位置添加日志,帮助调试布局问题:

build() {
  Column() {
    console.log('Column build called');
    // ...
  }
}

日志调试要点

  1. 生命周期方法:在build、aboutToAppear、aboutToDisappear中添加日志
  2. 状态变化:在状态变化的地方添加日志
  3. 性能监控:记录渲染时间和状态更新次数

11.3 使用LayoutInspector

LayoutInspector是HarmonyOS提供的布局检查工具:

  1. 查看组件树:查看页面的组件层次结构
  2. 检查属性:查看组件的尺寸、位置、样式属性
  3. 性能分析:分析渲染性能和布局计算时间

十二、实战案例:游戏页面布局

12.1 数独游戏页面布局

数独游戏页面是本项目中布局最复杂的页面之一:

build() {
  Column({ space: 8 }) {
    // 顶部导航栏
    Row() {
      Button('← 返回')
        .fontSize(14)
        .fontColor('#FFD700')
        .backgroundColor('transparent')
        .borderColor('#FFD700')
        .borderWidth(1)
        .padding({ left: 12, right: 12 })
        .onClick(() => {
          this.stopTimer();
          router.back();
        })

      Text('第 ' + this.currentLevel + ' 关')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFD700')

      Button('⏸ 暂停')
        .fontSize(14)
        .fontColor('#FFD700')
        .backgroundColor('transparent')
        .borderColor('#FFD700')
        .borderWidth(1)
        .padding({ left: 12, right: 12 })
        .onClick(() => this.togglePause())
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)
    .padding({ left: 20, right: 20 })
    .margin({ top: 40 })

    // 计时器
    Text(this.formatTime(this.timerSeconds))
      .fontSize(24)
      .fontColor('#87CEEB')

    // 数独网格
    Column() {
      Grid() {
        ForEach(this.cellValues, (value: number, index: number) => {
          GridItem() {
            Column() {
              if (value !== 0) {
                Text(value.toString())
                  .fontSize(this.boardSize === 4 ? 28 : (this.boardSize === 6 ? 22 : 18))
                  .fontWeight(FontWeight.Bold)
                  .fontColor(this.cellIsError[index] ? '#FF6B6B' : (this.cellIsOriginal[index] ? '#FFD700' : '#87CEEB'))
              }
            }
            .width('100%')
            .height('100%')
            .backgroundColor(this.cellIsSelected[index] ? 'rgba(255, 215, 0, 0.25)' : 'rgba(10, 25, 47, 0.9)')
            .borderWidth(this.getBorderWidth(index))
            .borderColor(this.getBorderColor(index))
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
            .onClick(() => this.selectCell(index))
          }
        })
      }
      .width('80%')
      .aspectRatio(1)
      .backgroundColor('#0A192F')
      .borderWidth(3)
      .borderColor('#FFD700')
      .borderRadius(16)
      .columnsTemplate(this.getColumnsTemplate())
      .rowsTemplate(this.getRowsTemplate())
    }

    // 数字键盘和功能按钮
    Column() {
      Grid() {
        ForEach(this.getNumberList(), (num: number) => {
          GridItem() {
            Button(num.toString())
              .fontSize(this.boardSize === 4 ? 24 : (this.boardSize === 6 ? 20 : 16))
              .fontColor('#FFD700')
              .backgroundColor('rgba(255, 215, 0, 0.1)')
              .borderColor('#FFD700')
              .borderWidth(1)
              .borderRadius(8)
              .width('100%')
              .height('100%')
              .onClick(() => this.inputNumber(num))
          }
        })
      }
      .width('90%')
      .height(this.boardSize === 9 ? 160 : 100)
      .columnsTemplate(this.boardSize === 4 ? '1fr 1fr' : '1fr 1fr 1fr')
      .rowsTemplate(this.boardSize === 4 ? '1fr 1fr' : (this.boardSize === 6 ? '1fr 1fr' : '1fr 1fr 1fr'))

      Row({ space: 16 }) {
        Button('❌ 擦除')
          .fontSize(14)
          .fontColor('#FF6B6B')
          .backgroundColor('rgba(255, 107, 107, 0.2)')
          .borderColor('#FF6B6B')
          .borderWidth(1)
          .borderRadius(8)
          .flexGrow(1)
          .height(40)
          .onClick(() => this.clearCell())

        Button('↪ 重做')
          .fontSize(14)
          .fontColor('#FFD700')
          .backgroundColor('transparent')
          .borderColor('#FFD700')
          .borderWidth(1)
          .borderRadius(8)
          .flexGrow(1)
          .height(40)
          .onClick(() => this.redo())
      }
      .width('90%')
      .padding({ top: 8 })

      Row({ space: 16 }) {
        Button('💡 提示')
          .fontSize(14)
          .fontColor('#0A192F')
          .backgroundColor('#FFD700')
          .borderRadius(8)
          .flexGrow(1)
          .height(40)
          .onClick(() => this.showHint())

        Button('↩ 撤销')
          .fontSize(14)
          .fontColor('#FFD700')
          .backgroundColor('transparent')
          .borderColor('#FFD700')
          .borderWidth(1)
          .borderRadius(8)
          .flexGrow(1)
          .height(40)
          .onClick(() => this.undo())
      }
      .width('90%')
      .padding({ top: 8 })
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#0A192F')
  .padding({ bottom: 80 })
}

布局结构分析

  1. 顶部导航栏:返回按钮、关卡信息、暂停按钮
  2. 计时器:大号天蓝色字体显示
  3. 数独网格:使用Grid组件,动态边框,响应式字体大小
  4. 数字键盘:根据网格尺寸调整布局
  5. 功能按钮:两行按钮组,分别为擦除/重做和提示/撤销

12.2 游戏列表页面布局

游戏列表页面展示11个游戏的入口:

build() {
  Column() {
    Column({ space: 8 }) {
      Text('九宫数独')
        .fontSize(36)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFD700')
      Text('10款数字能力训练 + 九宫数独终极挑战')
        .fontSize(16)
        .fontColor('#87CEEB')
    }
    .padding({ top: 80, bottom: 20 })

    Scroll() {
      Column({ space: 16 }) {
        ForEach(this.categories, (category: GameCategory) => {
          Button() {
            Row({ space: 16 }) {
              Column() {
                Text(category.icon)
                  .fontSize(32)
              }
              .width(60)
              .height(60)
              .backgroundColor('rgba(255, 215, 0, 0.1)')
              .borderColor(category.color)
              .borderWidth(1)
              .borderRadius(12)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)

              Column({ space: 4 }) {
                Row({ space: 8 }) {
                  Text(category.name)
                    .fontSize(18)
                    .fontWeight(FontWeight.Medium)
                    .fontColor('#FFFFFF')
                  Text(this.getDifficultyLabel(category.difficulty))
                    .fontSize(10)
                    .fontColor(this.getDifficultyColor(category.difficulty))
                    .backgroundColor('rgba(255, 255, 255, 0.1)')
                    .borderRadius(10)
                    .padding({ left: 6, right: 6 })
                }
                Text(category.description)
                  .fontSize(12)
                  .fontColor('#87CEEB')
                Text(category.abilities)
                  .fontSize(10)
                  .fontColor('#A0A0A0')
                  .maxLines(1)
              }
              .flexGrow(1)
            }
            .width('100%')
          }
          .width('90%')
          .height(90)
          .padding({ left: 8, right: 8 })
          .backgroundColor('rgba(255, 255, 255, 0.05)')
          .borderColor('rgba(255, 215, 0, 0.3)')
          .borderWidth(1)
          .borderRadius(16)
          .onClick(() => {
            this.showGameDialog(category);
          })
        })
      }
      .padding({ top: 16, bottom: 280 })
    }
    .width('100%')
    .flexGrow(1)
    .backgroundColor('#0A192F')
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#0A192F')
  .padding({ bottom: 60 })
}

布局结构分析

  1. 标题区域:大号金色标题 + 天蓝色副标题
  2. 滚动列表:可滚动的游戏卡片列表
  3. 游戏卡片:图标 + 名称/难度标签 + 描述 + 能力标签
  4. 卡片样式:半透明背景、金色边框、圆角设计

十三、常见问题与解决方案

13.1 Grid组件不显示

问题:Grid组件设置了columnsTemplate但内容不显示

解决方案

// 错误:没有设置高度
Grid() {
  ForEach(data, (item) => {
    GridItem() { /* ... */ }
  })
}
.columnsTemplate('1fr 1fr')

// 正确:设置固定高度或aspectRatio
Grid() {
  ForEach(data, (item) => {
    GridItem() { /* ... */ }
  })
}
.width('100%')
.height(300)  // 设置固定高度
.columnsTemplate('1fr 1fr')

// 或者使用aspectRatio
Grid() { /* ... */ }
.width('100%')
.aspectRatio(1)  // 保持正方形

13.2 数组更新不触发UI刷新

问题:修改数组元素后UI没有更新

解决方案

// 错误:直接修改数组元素
this.grid[index] = value;

// 正确:创建新数组引用
this.grid[index] = value;
this.grid = this.grid.slice();

// 或者使用push后重新赋值
this.items.push(newItem);
this.items = this.items.slice();

13.3 页面内容溢出

问题:页面内容超出屏幕边界

解决方案

// 错误:没有使用Scroll包裹内容
Column() {
  // 大量内容
}
.height('100%')

// 正确:使用Scroll包裹可滚动内容
Column() {
  Scroll() {
    Column() {
      // 大量内容
    }
  }
  .flexGrow(1)
}
.height('100%')

13.4 按钮点击不响应

问题:按钮点击没有触发事件

解决方案

// 错误:Button嵌套在不可点击的组件中
Column() {
  Button('点击')
    .onClick(() => { /* ... */ })
}
// Column没有设置onClick,但Button应该可以点击

// 检查点:
// 1. Button是否被其他组件覆盖
// 2. 是否有条件渲染导致Button不可见
// 3. onClick函数是否正确绑定

13.5 字体大小不适应不同屏幕

问题:字体在小屏幕上太大,大屏幕上太小

解决方案

// 使用sp单位(可缩放像素)
Text('文字')
  .fontSize(18)  // 默认使用sp单位

// 根据屏幕尺寸动态调整
Text('文字')
  .fontSize(this.screenWidth < 360 ? 14 : 18)

// 使用百分比
Text('文字')
  .fontSize('5%')  // 相对于父容器

十四、总结

UI布局与样式设计是HarmonyOS应用开发的核心环节,本项目通过以下实践打造了优秀的用户体验:

  1. 统一主题配色:深蓝色背景配金色主题,营造专业、高端的视觉感受
  2. 响应式布局:根据网格尺寸动态调整字体大小、键盘高度和列数
  3. 组件复用:提取通用组件,避免重复代码,提高开发效率
  4. 状态驱动UI:使用@State装饰器实现响应式更新,确保UI与数据同步
  5. 性能优化:合理使用flexGrow、避免过度渲染、优化数组更新
  6. 无障碍设计:确保文字对比度、按钮尺寸和焦点管理符合无障碍标准

通过本项目的实践,可以深入理解ArkUI的布局系统、状态管理机制和样式设计技巧。这些技术要点不仅适用于游戏应用,也可以应用于其他类型的HarmonyOS应用开发中。

在实际开发中,需要注意以下几点:

  1. 遵循设计规范:保持页面布局的一致性和规范性
  2. 测试不同设备:确保在不同屏幕尺寸和分辨率下都能正常显示
  3. 优化性能:避免布局抖动和过度渲染
  4. 关注用户体验:按钮尺寸、间距、颜色对比度等都直接影响用户体验

希望本文的内容能够帮助开发者更好地掌握HarmonyOS ArkTS的UI布局与样式设计技巧,打造出更加优秀的应用界面。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐