一、引言

手机锁屏是与用户接触最频繁的交互界面之一。从传统的四位 PIN 码到指纹识别再到面容解锁,锁屏方式在不断演进。在所有这些方案中,图案锁(Pattern Lock) 凭借其简单直观的"连线"交互,在全球范围内拥有广泛的用户基础——你不需要记忆数字,只需要在 3×3 的网格上滑出一个图案。

ArkUI 提供了 PatternLock 组件,将这种经典的图案解锁交互原生封装为一个声明式组件。开发者无需自己管理触摸事件、绘制路径、计算网格坐标——PatternLock 已经处理了从触摸追踪到路径绘制、从视觉反馈到状态管理的全部底层逻辑。

PatternLock 的典型应用场景远不止锁屏:

  • 儿童模式验证:防止儿童访问敏感设置
  • 隐私相册入口:为私密内容增设第二道防线
  • 支付确认:用图案代替密码确认交易
  • 家长控制:为特定功能设置访问限制

本文将通过一个完整的**“图案锁屏创建-确认-验证”**三阶段流程,深入解析 PatternLock 的每一个核心 API。阅读完本文,你将能够:

  • 掌握 PatternLock 组件的构造、样式配置和事件回调
  • 理解 onPatternComplete 回调中的 pattern 数组结构
  • 学会使用 PatternLockController 控制组件状态
  • 实现创建→确认→验证的完整锁屏流程
  • 运用 setChallengeResult 实现正确/错误的视觉反馈

二、PatternLock 组件核心 API 详解

2.1 组件概述

PatternLock 在屏幕上渲染一个正方形的 3×3 点阵。用户通过手指在点之间滑动来"绘制"图案——每经过一个点,该点被激活并加入当前的图案序列,同时点与点之间绘制一条连接线。当用户手指离开屏幕时,图案输入完成。

网格中 9 个点的编号如下:

0   1   2
3   4   5
6   7   8

一个从左上角开始、经过中心再到右下角的图案,其 input 数组为 [0, 4, 8]。一个 Z 字形的图案为 [0, 1, 2, 4, 6, 7, 8]

PatternLock 的构造函数接受一个可选参数:

PatternLock(controller?: PatternLockController)

PatternLockController 是图案锁的控制器,提供两个关键方法:

  • reset():重置组件状态,清除当前绘制的图案,使组件回到初始状态。
  • setChallengeResult(result: PatternLockChallengeResult):设置验证结果,用于向用户显示正确(CORRECT)或错误(WRONG)的视觉反馈。调用后,组件会短暂显示绿色或红色的闪烁效果。

2.2 样式属性

PatternLock 提供了一套完整的视觉定制 API,让你可以根据应用的设计风格自由调整外观:

PatternLock(this.controller)
  .sideLength(280)            // 组件宽度与高度(默认 288vp)
  .circleRadius(14)           // 网格点的半径(默认 6vp)
  .pathStrokeWidth(14)        // 连接线的粗细(默认 12vp)
  .regularColor('#B0B8C8')    // 未选中点的颜色
  .selectedColor('#1677FF')   // 选中点的颜色
  .activeColor('#69B1FF')     // 当前触摸点的激活色
  .pathColor('#1677FF')       // 连接线的颜色
  .backgroundColor('#1C2536') // 组件背景色

逐项说明:

  • sideLength:控制组件的整体尺寸。这是一个正方形组件,宽度和高度相等。默认 288vp 适合大多数手机屏幕。在我们的锁屏 Demo 中设为 280vp,在深色背景上有足够的视觉冲击力。

  • circleRadius:控制网格点的半径。加大半径可以让点更容易被触摸到(对于手指较粗的用户更友好),但过大会导致点与点之间的间隙变小,影响视觉清晰度。14vp 在 280vp 的网格中约占据 10% 的网格单元格宽度。

  • pathStrokeWidth:连接线的宽度。14vp 的线宽在视觉上比较显眼,在深色背景上清晰可见。较粗的线宽还有一个好处——用户在快速滑动时不需要精确经过每个点的中心,线宽增加了容错范围。

  • 颜色体系:PatternLock 使用四种颜色定义不同状态下的视觉效果。regularColor 是暗灰色,代表"尚未经过的点";selectedColor 是蓝色,代表"已选中的点";activeColor 是浅蓝色,代表"当前手指位置附近被高亮的点";pathColor 是连接线的颜色。这四种颜色的配合产生了丰富的视觉层次——用户通过颜色就能判断哪些点已经被连接、当前手指在什么位置。

2.3 autoReset 属性

.autoReset(true)  // 默认值,输入完成后自动清除图案

autoReset 控制图案输入完成后是否自动清除。当设为 true(默认值)时,用户手指离开屏幕后,图案会短暂显示(用于视觉确认),然后自动消失。当设为 false 时,图案会一直保留,直到手动调用 controller.reset()

在我们的 Demo 中,使用默认的 autoReset(true) 并结合 setTimeout 延迟调用 controller.reset(),在图案短暂显示后自动清除,给用户足够的视觉反馈时间。

2.4 onPatternComplete 回调

.onPatternComplete((input: Array<number>) => {
  // input: 用户绘制的图案数组,如 [0, 4, 8, 7]
  this.onPatternComplete(input);
})

onPatternComplete 是 PatternLock 最重要的回调。当用户完成图案绘制(手指离开屏幕)时触发,参数是一个数字数组,按照用户触摸的顺序排列。每个数字对应网格中的一个点(0-8)。

在我们的实现中,onPatternComplete 是驱动整个锁屏流程的引擎——根据当前的模式(创建/确认/验证),执行不同的逻辑分支。

2.5 onDotConnect 回调

.onDotConnect(callback: Callback<number>)

onDotConnect 在用户的手指每经过一个新的点时触发,参数是该点的编号(0-8)。这个回调用于实时追踪图案的绘制过程,通常用于以下场景:

  • 实时显示已连接点的数量
  • 在点被激活时播放触觉反馈
  • 限制图案的最小长度(在连接第 N 个点之前提示用户)

在我们的 Demo 中未使用此回调,因为图案验证只关心最终结果而非实时过程。但如果你需要在绘制过程中提供实时反馈,这个回调是必不可少的。
在这里插入图片描述

三、实战:图案锁屏完整流程

3.1 三阶段流程设计

我们的图案锁屏 Demo 实现了完整的创建→确认→验证三阶段流程:

  1. 创建阶段(mode: ‘create’):用户首次绘制一个解锁图案。图案必须至少包含 4 个点。绘制完成后,图案被保存到内存变量中。

  2. 确认阶段(mode: ‘confirm’):用户再次绘制相同的图案来确认。如果两次绘制一致,进入验证阶段;如果不同,回到创建阶段重新开始。

  3. 验证阶段(mode: ‘verify’):用户绘制已设定的图案来进行解锁。如果匹配,显示"解锁成功";如果不匹配,消耗一次尝试机会(总共 5 次),超过次数后锁定。

这种三阶段设计模拟了真实锁屏的首次设置和使用流程,让 Demo 在演示层面有完整的业务闭环。

3.2 状态管理

@State mode: string = 'create';            // 当前阶段
@State message: string = '请绘制解锁图案...'; // 提示消息
@State messageColor: string = '#86909C';   // 消息颜色
@State attemptCount: number = 0;           // 验证错误次数
@State unlocked: boolean = false;          // 是否已解锁(隐藏 PatternLock)
private firstPattern: Array<number> = [];  // 第一次绘制的图案
private controller: PatternLockController = new PatternLockController();

这里的关键设计决策:

  • mode 使用 @State:因为 mode 的变化直接影响 UI 显示(消息文本、步骤指示器等),需要触发 UI 刷新。
  • controller 使用 private:PatternLockController 是一个原生对象,不需要也不应该被 @State 追踪。这与 SwiperController、TabsController 的模式一致。
  • firstPattern 使用 private:它是内部校验数据,不直接渲染到 UI 上(UI 通过 mode 间接反映),因此不需要 @State

3.3 核心回调逻辑

onPatternComplete(input: Array<number>): void {
  // 1. 长度校验
  if (input.length < 4) {
    this.message = '至少需要连接 4 个点,请重新绘制';
    this.messageColor = AppColors.ERROR;
    this.controller.setChallengeResult(PatternLockChallengeResult.WRONG);
    this.resetAfterDelay();
    return;
  }

  if (this.mode === 'create') {
    // 2. 首次创建:保存图案,切换到确认模式
    this.firstPattern = input;
    this.mode = 'confirm';
    this.message = '请再次绘制相同图案以确认';
    this.messageColor = AppColors.PRIMARY;
    this.resetAfterDelay();
  } else if (this.mode === 'confirm') {
    // 3. 确认图案:对比两次输入
    if (this.patternsMatch(input, this.firstPattern)) {
      this.mode = 'verify';
      this.message = '图案设置成功!请绘制图案验证解锁';
      this.messageColor = '#52C41A';
      this.controller.setChallengeResult(PatternLockChallengeResult.CORRECT);
      this.resetAfterDelay();
    } else {
      this.message = '两次绘制不一致,请重新设置图案';
      this.messageColor = AppColors.ERROR;
      this.controller.setChallengeResult(PatternLockChallengeResult.WRONG);
      this.mode = 'create';
      this.firstPattern = [];
      this.resetAfterDelay();
    }
  } else if (this.mode === 'verify') {
    // 4. 验证解锁
    if (this.patternsMatch(input, this.firstPattern)) {
      this.unlocked = true;
      this.message = '✅ 解锁成功!';
      this.controller.setChallengeResult(PatternLockChallengeResult.CORRECT);
    } else {
      this.attemptCount++;
      const left: number = 5 - this.attemptCount;
      if (left <= 0) {
        this.message = '❌ 错误次数过多,请重新设置图案';
        this.unlocked = true; // 触发锁定态
      } else {
        this.message = `❌ 图案错误,还剩 ${left} 次机会`;
        this.controller.setChallengeResult(PatternLockChallengeResult.WRONG);
        this.resetAfterDelay();
      }
    }
  }
}

此方法是一个典型的状态机实现——根据当前的 mode 决定如何处理输入。让我们按分支深入分析:

分支 1:长度校验。在三个模式中,我们都要求图案至少包含 4 个点。少于 4 个点的图案安全性太低(3 个点的图案组合太少,容易被猜到),4 个点被认为是最低安全标准。如果图案太短,通过 setChallengeResult(WRONG) 显示红色闪烁提示,并在 600ms 后自动重置组件。

分支 2:创建模式。首次绘制成功后,将图案保存到 firstPattern,模式切换到 confirm,消息引导用户再次绘制。这里 resetAfterDelay() 在短暂延迟后清除 PatternLock,让组件为第二次输入做好准备。

分支 3:确认模式。通过 patternsMatch() 逐位比较两个图案数组。这里不能直接使用 ===JSON.stringify 比较数组——ArkTS 中两个数组即使内容相同,引用也不同。最稳妥的比较方式是逐元素对比长度和每个位置的值。

如果确认成功,调用 setChallengeResult(CORRECT) 显示绿色闪烁,告诉用户"操作正确"的视觉反馈。如果确认失败,不仅清空 firstPattern,还将模式重置回 create,让用户从头开始。

分支 4:验证模式。这是正常使用阶段的入口。正确则 unlocked = true(隐藏 PatternLock,显示成功页面);错误则消耗一次尝试机会。5 次尝试的限制是仿照真实系统锁屏的安全策略——防止暴力破解。达到上限后 unlocked = true,但显示的是"已锁定"态(需重新设置)而非"欢迎"态。
在这里插入图片描述

3.4 图案比较算法

patternsMatch(a: Array<number>, b: Array<number>): boolean {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

比较两个图案时,需要满足两个条件:

  1. 长度相等:两个图案必须连接了相同数量的点。长度不同意味着图案不同。
  2. 顺序一致:每个位置上的点编号必须相同。顺序是图案身份的核心——[0, 4, 8][8, 4, 0] 是不同的图案,尽管经过了相同的点。

3.5 步骤指示器设计

getStepIndicator(): string {
  if (this.mode === 'create') return '●●○○';
  if (this.mode === 'confirm') return '●●●○';
  if (this.mode === 'verify') return '●●●●';
  return '';
}

步骤指示器使用 ● 和 ○ 字符的排列直观地展示当前处于三阶段流程中的哪一步。这是一种简单但有效的 UX 设计——用户不需要阅读长段文字就能理解自己在哪里、还有几步。

结合下方的描述文字(“第 1 步:设置图案” / “第 2 步:确认图案” / “第 3 步:验证解锁”),构成了完整的进度指引系统。

3.6 延迟重置机制

resetAfterDelay(): void {
  setTimeout(() => {
    this.controller.reset();
  }, 600);
}

600ms 的延迟是一个经过权衡的值:

  • 太短(< 200ms):用户来不及看到自己绘制的图案,缺乏确认感
  • 太长(> 1000ms):用户会产生等待感,体验不够流畅
  • 600ms:恰好让图案在屏幕上停留一下,用户能确认自己画了什么,然后自然消失

3.7 重置与跳过功能

resetAll(): void {
  this.mode = 'create';
  this.message = '请绘制解锁图案(至少连接 4 个点)';
  this.messageColor = AppColors.TEXT_TERTIARY;
  this.firstPattern = [];
  this.attemptCount = 0;
  this.unlocked = false;
  this.controller.reset();
}

resetAll() 将所有状态变量恢复到初始值。这个方法在任何阶段都可以调用(底部有"重置图案"按钮),让用户可以随时从头开始。controller.reset() 确保 PatternLock 组件也清空显示。

"跳过设置"按钮是一个便捷的 Demo 入口——它直接将 unlocked 设为 true,用户无需完成整个流程就能看到"解锁后"的界面。这在 Demo 演示中非常实用,与真实应用中的"退出登录"或"关闭锁屏"功能对应。
在这里插入图片描述

四、进阶技巧与最佳实践

4.1 PatternLockController 不能是 @State

这是 PatternLock 使用中最常见的错误。PatternLockController 是一个内部包含原生调用方法的对象,将其声明为 @State 会导致框架尝试对其内部状态进行深拷贝和代理追踪,这不仅浪费性能,还可能在运行时产生未预期的行为。

正确做法是使用 private 声明:

private controller: PatternLockController = new PatternLockController();

这条规则同样适用于 SwiperControllerTabsControllerTextAreaController 等所有 ArkUI 控制器类。

4.2 图案存储的安全性

在我们的 Demo 中,firstPattern 存储在内存变量中。在真实应用中,你绝对不能将原始图案数组存储在客户端——这存在严重的安全风险。正确的做法是只存储图案的哈希值

// 设置图案时
const hash = sha256(input.join(','));  // 转为字符串后哈希
storage.set('pattern_hash', hash);      // 只存哈希

// 验证图案时
const attempt = sha256(input.join(','));
const stored = storage.get('pattern_hash');
const match = attempt === stored;

这样即使攻击者获取了本地存储的数据,也无法还原出原始图案。

4.3 最小图案长度的权衡

我们的 Demo 要求至少 4 个点。从安全角度看:

  • 1 个点:9 种可能(极其不安全)
  • 2 个点:约 56 种可能(不安全)
  • 3 个点:约 320 种可能(不够安全)
  • 4 个点:约 1624 种可能(基本安全)
  • 5 个点:约 7152 种可能(较安全)
  • 6-9 个点:显著提高,但用户体验下降

Android 的图案锁默认要求至少 4 个点,这是一个业界广泛接受的平衡点。如果你的应用处理的是高度敏感信息(如金融交易),建议要求至少 5-6 个点。

4.4 错误次数限制

5 次尝试后锁定的策略(我们的 Demo 中使用)是仿照 iOS 的"10 次错误后擦除"和 Android 的"5 次后锁定 30 秒"的中间方案。在生产环境中,你可能还需要加入:

  • 递增等待时间:第 1-3 次无延迟,第 4 次等待 30 秒,第 5 次等待 1 分钟
  • 强制密码回退:超过限制后要求输入备用密码
  • 账户锁定:将错误历史上传到服务器,由后台决定是否锁定账户

五、总结

本文以图案锁屏为业务场景,深入解析了 ArkUI PatternLock 组件的核心 API 和完整的创建→确认→验证三阶段流程。

回顾本文覆盖的核心要点:

  1. PatternLock 组件结构:9 个点的 3×3 网格,用户通过滑动连接点来绘制图案。组件自动处理触摸追踪、路径绘制和状态管理。

  2. 样式体系sideLength 控制尺寸,circleRadius 控制点大小,pathStrokeWidth 控制线宽,regularColor/selectedColor/activeColor/pathColor 四色体系定义不同状态的视觉效果。

  3. 核心回调onPatternComplete(input: Array<number>) 是图案输入完成的回调,input 数组按顺序记录了被连接点的编号(0-8)。

  4. 控制器能力PatternLockController 提供 reset() 重置组件状态和 setChallengeResult() 设置正确/错误视觉反馈。

  5. 三阶段流程:创建(首次绘制)→ 确认(再次绘制)→ 验证(解锁校验),通过 mode 状态变量驱动状态机切换。

  6. 安全机制:最小 4 点长度要求、图案精确对比算法、5 次错误尝试限制,以及生产环境中的哈希存储建议。

PatternLock 是 ArkUI 中对触摸交互封装程度最高的组件之一——它将复杂的多点触摸追踪、路径计算和视觉动画全部内聚在一个组件中。掌握 PatternLock,你就能在应用中轻松实现一个既安全又美观的图案认证系统。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐