《随机乱序键盘》四、简约风格随机乱序键盘案例指南

HarmonyOS NEXT 实战:基于状态管理V2的简约风格随机乱序键盘完整开发指南

适用版本 :HarmonyOS NEXT(API 23+)、DevEco Studio 6.1+

关键词:HarmonyOS NEXT、ArkTS、乱序键盘、状态管理V2、@ComponentV2、@ObservedV2、cryptoFramework、防截屏、customKeyboard


效果

一、项目概述

1.1 需求分析

在金融、支付等安全敏感场景中,密码输入键盘的按键位置如果固定不变,恶意程序可以通过记录用户的点击坐标来推算密码。本案例实现一个简约风格的随机乱序键盘,具备以下核心特性:

  • 安全随机乱序 :使用 cryptoFramework 密码学安全随机数 + Fisher-Yates 洗牌算法
  • 双键盘类型:ABC 英文键盘 + 数字键盘,支持类型切换
  • 大小写切换:Shift 键切换大小写,切换时重新乱序
  • 防截屏保护 :键盘弹出时自动开启 setWindowPrivacyMode
  • 状态管理V2 :全面使用 @ComponentV2@ObservedV2@Trace@Local@Param@Event 等新一代API
  • 简约UI风格:浅灰白底 + 蓝色主题色 + 大圆角 + 充裕留白

1.2 效果说明

功能 描述
页面打开 显示简约登录页,点击密码输入框弹出乱序键盘
字母键盘 26个字母随机排列,三排字母 + Shift/123/空格/完成
数字键盘 10个数字随机排列,三排数字 + 功能键行
键盘切换 点击 "123" 切换到数字键盘,点击 "ABC" 切回字母键盘,每次切换重新乱序
大小写 点击 Shift 切换大小写,同时重新乱序
防截屏 键盘显示期间,系统截屏/录屏结果中窗口区域显示为黑色

二、技术选型

2.1 核心技术栈

技术 用途
状态管理V2 @ObservedV2 + @Trace 管理数据模型,@ComponentV2 + @Local/@Param/@Event 构建组件
cryptoFramework @kit.CryptoArchitectureKit 提供密码学安全随机数
customKeyboard TextInput.customKeyboard() 替换系统键盘
setWindowPrivacyMode window.setWindowPrivacyMode() 实现防截屏
Fisher-Yates 算法 均匀随机打乱数组顺序

2.2 状态管理V2 vs V1 对比

特性 V1(@Component) V2(@ComponentV2)
组件装饰器 @Component @ComponentV2
局部状态 @State @Local
属性传递 @Prop @Param(父→子单向同步)
事件回调 @Link / EventEmitter @Event
数据观测 @Observed + @ObjectLink @ObservedV2 + @Trace
属性监听 @Watch @Monitor(方法装饰器)
计算属性 @Computed

本案例采用V2的优势

  • @ObservedV2 类可直接作为 @Param 传递给子组件,子组件自动响应 @Trace 属性变化
  • @Event 提供了类型安全的事件回调机制,替代复杂的 @Link 双向绑定
  • 更清晰的单向/双向数据流控制

⚠️ V2 装饰器语法注意@Provider()@Consumer() 必须加括号,@Local@Param@Trace@Event 不加括号,@Monitor 是方法装饰器(详见《ArkTS状态管理V2迁移踩坑与修复指南》)。


三、项目结构设计

复制代码
entry/src/main/
├── ets/
│   ├── entryability/
│   │   └── EntryAbility.ets            # Ability 入口
│   ├── pages/
│   │   ├── Index.ets                   # 首页(导航入口)
│   │   ├── NavigationPage.ets         # 沉浸光感导航页
│   │   └── LoginPage.ets              # 登录页(集成乱序键盘)
│   ├── outorderkeyboard/              # ★ 核心模块
│   │   ├── KeyboardController.ets     # 控制器(@ObservedV2 + @Trace)
│   │   ├── OutOrderKeyboard.ets       # 键盘容器(防截屏集成)
│   │   ├── ABCKeyboardComponent.ets   # ABC英文键盘组件
│   │   ├── NumKeyboardComponent.ets   # 数字键盘组件
│   │   ├── SymbolKeyboadComponent.ets # 符号键盘组件
│   │   ├── KeyboardConstants.ets      # 常量定义
│   │   └── KeyboardData.ets           # 枚举类型定义
│   └── common/
│       └── CommonConstants.ets        # 公共常量
├── resources/
│   ├── base/
│   │   ├── element/
│   │   │   ├── string.json             # 字符串资源
│   │   │   ├── color.json              # 颜色资源
│   │   │   └── float.json              # 尺寸资源
│   │   ├── media/
│   │   │   ├── ic_delete.png           # 删除图标
│   │   │   ├── ic_shift_upper.png      # Shift大写图标
│   │   │   ├── ic_shift_lower.png      # Shift小写图标
│   │   │   └── ic_space.png            # 空格图标
│   │   └── profile/
│   │       └── main_pages.json         # 页面路由配置
│   └── module.json5                    # 模块配置(含隐私权限)

四、核心实现详解

4.1 安全随机数生成与数组乱序

文件SimpleKeyboardController.ets

乱序是整个键盘安全性的核心。我们使用 cryptoFramework 生成密码学安全随机数,配合 Fisher-Yates 洗牌算法实现均匀乱序。

typescript 复制代码
import { cryptoFramework } from '@kit.CryptoArchitectureKit';

@ObservedV2
export class SimpleKeyboardController {
  // 复用随机数生成器实例,避免频繁创建
  private rand: cryptoFramework.Rand = cryptoFramework.createRandom();

  /**
   * Fisher-Yates 洗牌算法
   * 从数组末尾开始,每次随机选一个元素与当前位置交换
   * 保证每个排列出现的概率相等(1/n!)
   */
  shuffle<T>(arr: T[]): T[] {
    let result: T[] = [...arr]; // 浅拷贝,不修改原数组
    let n = result.length;
    for (let i = n - 1; i > 0; i--) {
      let data = this.rand.generateRandomSync(1); // 生成1字节安全随机数
      let j = data.data[0] % (i + 1);             // 映射到 [0, i] 区间
      let temp = result[i];
      result[i] = result[j];
      result[j] = temp;
    }
    return result;
  }
}

要点解析

  • generateRandomSync(1) 生成1字节数据(0~255),除以255归一化即可得到 [0,1) 浮点数
  • Fisher-Yates 算法时间复杂度 O(n),远优于 Array.sort(() => random() - 0.5) 的 O(n·log n)
  • 复用 Rand 实例而非每次调用都创建新实例,减少对象创建开销

4.2 数据模型设计

文件SimpleKeyboardModel.ets

使用 @ObservedV2 + @Trace 装饰器构建响应式数据模型,当 @Trace 属性变化时,引用该属性的 @ComponentV2 组件会自动重新渲染。

typescript 复制代码
@ObservedV2
export class SimpleKeyboardModel {
  @Trace keyboardType: SimpleKeyboardType = SimpleKeyboardType.ABC;
  @Trace isShiftUpper: boolean = false;

  // ABC键盘三排数据
  @Trace abcRow1: string[] = [];
  @Trace abcRow2: string[] = [];
  @Trace abcRow3: string[] = [];

  // 数字键盘四排数据
  @Trace numRow1: string[] = [];
  @Trace numRow2: string[] = [];
  @Trace numRow3: string[] = [];
  @Trace numRow4: string[] = [];
}

设计考量

  • 将数据按排拆分(abcRow1/2/3),配合 ForEach 渲染,乱序时只需替换数组引用即可触发UI更新
  • keyboardTypeisShiftUpper 作为 @Trace 属性,驱动键盘容器的条件渲染

4.3 键盘控制器

文件SimpleKeyboardController.ets

控制器是核心枢纽,负责:输入逻辑、光标管理、乱序触发、键盘类型切换。

typescript 复制代码
@ObservedV2
export class SimpleKeyboardController {
  @Trace text: string = '';
  @Trace leftCaretPos: number = 0;
  @Trace rightCaretPos: number = 0;
  maxLength: number = 16;
  textInputController?: TextInputController;
  model: SimpleKeyboardModel = new SimpleKeyboardModel();

  /** 在光标位置插入文本 */
  insertText(value: string): string {
    let remaining = this.maxLength - this.text.length;
    if (remaining <= 0) return this.text;
    let insertStr = value.substring(0, remaining);
    let before = this.text.substring(0, this.leftCaretPos);
    let after = this.text.substring(this.rightCaretPos);
    this.text = before + insertStr + after;
    this.leftCaretPos += insertStr.length;
    this.rightCaretPos = this.leftCaretPos;
    this.updateCaret();
    return this.text;
  }

  /** 删除光标前一个字符 */
  deleteText(): string {
    if (this.leftCaretPos === this.rightCaretPos && this.leftCaretPos > 0) {
      this.leftCaretPos--;
    }
    let before = this.text.substring(0, this.leftCaretPos);
    let after = this.text.substring(this.rightCaretPos);
    this.text = before + after;
    this.rightCaretPos = this.leftCaretPos;
    this.updateCaret();
    return this.text;
  }

  /** 乱序ABC键盘 */
  shuffleABC(): void {
    let shuffled = this.shuffle(this.abcChars);
    if (this.model.isShiftUpper) {
      shuffled = shuffled.map((c: string) => c.toUpperCase());
    }
    this.model.abcRow1 = shuffled.slice(0, 10);
    this.model.abcRow2 = shuffled.slice(10, 19);
    this.model.abcRow3 = shuffled.slice(19, 26);
  }

  /** 切换键盘类型 */
  switchKeyboard(type: SimpleKeyboardType): void {
    this.model.keyboardType = type;
    if (type === SimpleKeyboardType.ABC) {
      this.shuffleABC();
    } else {
      this.shuffleNum();
    }
  }
}

核心逻辑

  • insertText:根据当前光标位置 leftCaretPosrightCaretPos 精确插入,支持在文本中间插入
  • deleteText:区分"光标未选中"和"有选中区间"两种情况
  • shuffleABC / shuffleNum:每次切换键盘类型或大小写时重新乱序

4.4 ABC键盘组件

文件SimpleAbcKeyboard.ets

使用 @ComponentV2 + @Param 接收控制器,@Event 传递点击事件。

typescript 复制代码
@ComponentV2
export struct SimpleAbcKeyboard {
  @Param controller: SimpleKeyboardController = new SimpleKeyboardController();

  build() {
    Column({ space: 6 }) {
      // 第一排:10个字母
      Row({ space: 6 }) {
        ForEach(this.controller.model.abcRow1, (item: string) => {
          AbcNormalKey({
            label: item,
            onKeyClick: (label: string) => { this.onLetterClick(label); }
          })
        }, (item: string) => item)
      }.width('100%')

      // 第二排:9个字母(左右缩进模拟阶梯布局)
      Row({ space: 6 }) {
        ForEach(this.controller.model.abcRow2, (item: string) => {
          AbcNormalKey({
            label: item,
            onKeyClick: (label: string) => { this.onLetterClick(label); }
          })
        }, (item: string) => item)
      }.width('100%').padding({ left: 16, right: 16 })

      // 第三排:Shift + 7字母 + Del
      // 第四排:123 | 空格 | 完成
      // ...
    }
  }
}

设计亮点

  • @Param controller 传入 @ObservedV2 对象,controller.model.abcRow1@Trace 属性变化时自动触发 ForEach 重新渲染
  • 使用子组件 AbcNormalKeyAbcFuncKey 分离普通键和功能键的样式
  • 第二排添加左右 padding 形成阶梯式布局,更接近真实键盘

4.5 数字键盘组件

文件SimpleNumKeyboard.ets

数字键盘采用 3+1 布局:3个数字键 + 1个功能键。

typescript 复制代码
@ComponentV2
export struct SimpleNumKeyboard {
  @Param controller: SimpleKeyboardController = new SimpleKeyboardController();

  @Builder
  NumRowWithFunc(rowData: string[], funcLabel: string) {
    Row({ space: 6 }) {
      Flex({ flex: 3 }) {
        Row({ space: 6 }) {
          ForEach(rowData, (item: string) => {
            NumKey({ label: item, onKeyClick: (l: string) => { this.onNumClick(l); } })
          }, (item: string) => item)
        }.width('100%')
      }
      Flex({ flex: 1 }) {
        NumFuncKey({ label: funcLabel, onKeyClick: () => { this.onFuncClick(funcLabel); } })
      }
    }.width('100%')
  }

  build() {
    Column({ space: 6 }) {
      this.NumRowWithFunc(this.controller.model.numRow1, '⌫')  // 第1排:3数字+Del
      this.NumRowWithFunc(this.controller.model.numRow2, 'ABC') // 第2排:3数字+ABC
      // ...
    }
  }
}

布局策略 :使用 Flexflex 属性实现 3:1 宽度比例,数字区域占 3 份,功能键占 1 份。


4.6 键盘容器与防截屏

文件SimpleKeyboard.ets

键盘容器负责类型切换渲染和防截屏集成:

typescript 复制代码
@ComponentV2
export struct SimpleKeyboard {
  @Param controller: SimpleKeyboardController = new SimpleKeyboardController();

  build() {
    Column() {
      if (this.controller.model.keyboardType === SimpleKeyboardType.ABC) {
        SimpleAbcKeyboard({ controller: this.controller })
      } else {
        SimpleNumKeyboard({ controller: this.controller })
      }
    }
    .width('100%')
    .height($r('app.float.simple_kb_height'))
    .backgroundColor($r('app.color.simple_kb_bg'))
    .onAppear(() => { this.setPrivacyMode(true); })   // 键盘弹出 → 开启防截屏
    .onDisAppear(() => { this.setPrivacyMode(false); }) // 键盘收起 → 关闭防截屏
  }
}

防截屏实现 :利用 onAppear / onDisAppear 生命周期钩子,在键盘显示/隐藏时自动切换隐私模式。


4.7 登录页面集成

文件SimpleLoginPage.ets

登录页面是最终的集成点,将 TextInput 与自定义键盘绑定:

typescript 复制代码
@ComponentV2
export struct SimpleLoginPage {
  @Local controller: SimpleKeyboardController = new SimpleKeyboardController();
  @Local textInputController: TextInputController = new TextInputController();

  aboutToAppear(): void {
    this.controller.textInputController = this.textInputController;
    this.controller.maxLength = 16;
    this.controller.init(); // 首次乱序
  }

  build() {
    Column() {
      TextInput({
        placeholder: '请输入密码',
        text: this.controller.text,
        controller: this.textInputController
      })
        .type(InputType.Password)
        .customKeyboard(this.keyboardBuilder(), { supportAvoidance: true })
        .onChange((value: string) => {
          this.controller.text = value;
          this.controller.updateCaret();
        })
        .onTextSelectionChange((start: number, end: number) => {
          this.controller.setCaretPosition(start, end);
        })
    }
  }

  @Builder
  keyboardBuilder() {
    SimpleKeyboard({ controller: this.controller })
  }
}

关键集成点

  1. customKeyboard(this.keyboardBuilder(), { supportAvoidance: true }) --- 绑定自定义键盘
  2. onChange --- 同步输入文本到控制器
  3. onTextSelectionChange --- 同步光标位置到控制器
  4. type(InputType.Password) --- 密码遮挡

五、资源配置

5.1 简约配色方案

文件resources/base/element/color.json

json 复制代码
{
  "name": "simple_page_bg",       "value": "#F7F8FA"   // 页面浅灰背景
},
{
  "name": "simple_card_bg",       "value": "#FFFFFF"   // 卡片纯白背景
},
{
  "name": "simple_primary",       "value": "#4F6EF7"   // 蓝色主题色
},
{
  "name": "simple_text_main",     "value": "#1A1A2E"   // 主文本深色
},
{
  "name": "simple_text_secondary","value": "#8E8E93"   // 次要文本灰色
},
{
  "name": "simple_kb_bg",         "value": "#D1D3D9"   // 键盘背景
},
{
  "name": "simple_key_normal",    "value": "#FFFFFF"   // 普通键白色
},
{
  "name": "simple_key_func",      "value": "#AEB3BF"   // 功能键灰色
}

5.2 权限配置

文件module.json5

json5 复制代码
"requestPermissions": [
  {
    "name": "ohos.permission.PRIVACY_WINDOW"  // 防截屏权限声明
  }
]

六、运行与调试

6.1 环境要求

  • DevEco Studio:5.0 及以上版本
  • HarmonyOS SDK:API 12+
  • 测试设备:HarmonyOS NEXT 真机或模拟器

6.2 运行步骤

  1. 使用 DevEco Studio 打开项目
  2. 同步项目依赖(File → Sync and Refresh Project)
  3. 选择目标设备,点击运行
  4. 首页点击"进入演示"按钮,跳转到简约登录页
  5. 点击密码输入框,弹出乱序键盘
  6. 尝试截屏,验证防截屏功能

6.3 常见问题

问题 解决方案
键盘不弹出 检查 customKeyboard@Builder 是否通过 this.builderName() 调用
乱序不触发 确认 controller.init()aboutToAppear 中调用
防截屏无效 检查 module.json5 中是否声明了 ohos.permission.PRIVACY_WINDOW
V2 API 报错 确认 DevEco Studio 版本 ≥ 5.0,SDK API ≥ 12

七、总结与扩展

7.1 项目亮点

亮点 说明
安全随机 cryptoFramework 密码学安全随机 + Fisher-Yates 均匀分布
V2状态管理 全面使用 @ComponentV2 / @ObservedV2 / @Trace / @Param / @Event
防截屏 键盘生命周期自动管理隐私模式
零图标依赖 使用 Unicode 字符(⌫、⇧)和纯文字替代图标资源
简约设计 浅灰白底 + 蓝色主题 + 大圆角 + 卡片式布局

7.2 可扩展方向

  • 符号键盘:添加第三种键盘类型,支持特殊字符输入
  • 长按连续删除 :通过 LongPressGesture({ repeat: true, duration: 50 }) 实现
  • 键盘音效/震动:在按键点击时添加触觉反馈
  • 深色模式 :添加 resources/dark/element/color.json 适配深色主题
  • 多语言支持:扩展字母表支持拼音、俄文等更多字符集
  • 动画效果:按键乱序时添加过渡动画,提升视觉体验

-|------|

| 安全随机 | cryptoFramework 密码学安全随机 + Fisher-Yates 均匀分布 |

| V2状态管理 | 全面使用 @ComponentV2 / @ObservedV2 / @Trace / @Param / @Event |

| 防截屏 | 键盘生命周期自动管理隐私模式 |

| 零图标依赖 | 使用 Unicode 字符(⌫、⇧)和纯文字替代图标资源 |

| 简约设计 | 浅灰白底 + 蓝色主题 + 大圆角 + 卡片式布局 |

7.2 可扩展方向

  • 符号键盘:添加第三种键盘类型,支持特殊字符输入
  • 长按连续删除 :通过 LongPressGesture({ repeat: true, duration: 50 }) 实现
  • 键盘音效/震动:在按键点击时添加触觉反馈
  • 深色模式 :添加 resources/dark/element/color.json 适配深色主题
  • 多语言支持:扩展字母表支持拼音、俄文等更多字符集
  • 动画效果:按键乱序时添加过渡动画,提升视觉体验
相关推荐
w139548564223 小时前
鸿蒙实战:PhoneLoginPage 6位验证码 Cell 设计与 60 秒倒计时
华为·harmonyos·鸿蒙系统
xd1855785553 小时前
敲门砖工坊-求职信定制的HarmonyOS开发实践
人工智能·华为·harmonyos·鸿蒙
心中有国也有家4 小时前
AtomGit Flutter 鸿蒙客户端:ModalBottomSheet 实战
android·javascript·学习·flutter·华为·harmonyos
tyqtyq224 小时前
求职信生成:AI 智能求职信撰写系统的鸿蒙实现
人工智能·学习·华为·生活·harmonyos
绝世番茄4 小时前
鸿蒙原生ArkTS布局方式之Brightness亮度调节布局详解
华为·harmonyos·鸿蒙
l134062082355 小时前
鸿蒙实战:RDB 数据库设计与 DatabaseService
数据库·华为·harmonyos·鸿蒙系统
ZZZMMM.zip5 小时前
数据侦探社-数据趋势分析的HarmonyOS开发实践
人工智能·华为·harmonyos·鸿蒙·鸿蒙系统
星释7 小时前
鸿蒙智能体开发实战:34.鸿蒙壁纸大师 - 提示词工程与优化
android·华为·harmonyos·鸿蒙
千逐687 小时前
React Native 性能优化深度解析:从原理到实战
flutter·华为·鸿蒙