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更新 keyboardType和isShiftUpper作为@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:根据当前光标位置leftCaretPos和rightCaretPos精确插入,支持在文本中间插入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重新渲染- 使用子组件
AbcNormalKey和AbcFuncKey分离普通键和功能键的样式 - 第二排添加左右
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
// ...
}
}
}
布局策略 :使用 Flex 的 flex 属性实现 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 })
}
}
关键集成点:
customKeyboard(this.keyboardBuilder(), { supportAvoidance: true })--- 绑定自定义键盘onChange--- 同步输入文本到控制器onTextSelectionChange--- 同步光标位置到控制器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 运行步骤
- 使用 DevEco Studio 打开项目
- 同步项目依赖(File → Sync and Refresh Project)
- 选择目标设备,点击运行
- 首页点击"进入演示"按钮,跳转到简约登录页
- 点击密码输入框,弹出乱序键盘
- 尝试截屏,验证防截屏功能
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适配深色主题 - 多语言支持:扩展字母表支持拼音、俄文等更多字符集
- 动画效果:按键乱序时添加过渡动画,提升视觉体验