Flyme 的锁屏数字密码可以按住不放、划过即输入。HyperOS 没有这个能力,而且它不是配置项 —— 只能挂代码。
项目地址:https://github.com/z7ui/PinSwipe | 三星版:https://github.com/z7ui/PinSwipeForGalaxy
一句话说清它做什么
锁屏输数字密码时,手指按在第一位数字上不抬起来,一路划过去,按划过的顺序依次输入。就是 Flyme 那个「滑动输入密码」。
不需要改系统 APK:一个普通 LSPosed 模块,作用域只有 SystemUI。小米 17 Pro / HyperOS 4(Android 17 / SDK 37)实测可用。
实现思路
反编译 /system_ext/priv-app/MiuiSystemUI/MiuiSystemUI.apk(OS4.0.0.41.XBLCNXM)后,键盘结构很清晰:
com.android.keyguard.KeyguardPINView ← 根,extends KeyguardPinBasedInputView
├─ AlphaOptimizedRelativeLayout
│ ├─ PasswordTextViewForPIN id 0x7f0b0912(那排圆点)
│ └─ NumPadButton id 0x7f0b05d0(确定键)
├─ 4 × LinearLayout
│ └─ NumPadKey × 10 id 0x7f0b05c6..0x7f0b05cf
└─ FrameLayout id 0x7f0b0369(⌫/返回)
挂点选在 com.android.keyguard.NumPadKey#onTouchEvent(MotionEvent)。原因是:数字键一旦收到 ACTION_DOWN 就成为这次手势的 touch target,之后每一个 MOVE 都会送到它手里,哪怕手指早就跑到别的键上去了。从这里旁观能拿到整段手势的完整轨迹,而且不改参数、不吞事件。
输入动作直接调按键自己的 performClick()。这不是我发明的 —— ROM 自己处理外接键盘时就是这么干的(KeyguardPinBasedInputView#onKeyDown):
if (i >= 7 && i <= 16) { buttons[i - 7].performClick(); return true; }
点击最终落到 NumPadKey$mListener$1#onClick:把数字 append() 到 PasswordTextViewForPIN,并调一次 PowerManager.userActivity()。所以不碰任何私有输入逻辑,也不绕过 isEnabled() 检查。
为什么不会把「点按」搞坏
这是整个模块最关键的设计。原生 View.onTouchEvent 的规则是:按下后只要手指没离开该键的 touch slop,抬手时才会 performClick();一旦离开 slop,按下状态就被取消,抬手什么都不会输入。
也就是说「划过去」这个动作,系统那一路是一声不吭的 —— 正好是我们要补的。补的规则只有四条:
| 时刻 | 动作 |
|---|---|
| 手指离开起点键的 slop | 补上起点键(这时系统那一路已彻底失效,补了不会重复) |
| 手指进入一个新的键 | 补上一个键;新键挂起 |
| 抬手 | 如果系统还会自己点起点键(手指没离开过 slop),什么都不做,交给系统 |
| 抬手 | 否则补上最后挂起的键 |
于是单点一下仍然只输入一位,划过 1→2→3 会依次输入 1、2、3。实测:三次独立点按 = 正好 3 位,不翻倍。
快速划动时相邻两次事件可能跨过好几个键,所以坐标之间做了插值采样,避免漏键。
长按 ⌫ 清空:手指停在 ⌫ 上超过系统长按时间(默认 500ms),会另外回一次 onKeyHeld,并且这一下不再补短按。ROM 自己在 KeyguardPinBasedInputViewController#onViewAttached 里就绑了短按删一位、长按 resetPasswordText(true, true) 清空,模块这边只是调它自己的 performLongClick(),语义完全一致。
两个从真机数据里抠出来的坑
一、斜着划会多输数字
真机(Xiaomi 17 Pro / HyperOS 4)量出来的键盘几何:
键 321 x 192
横向 128 / 449 / 770 —— 正好等于键宽,横向完全无缝
纵向 941 / 1150 / 1359 —— 行间只有 17px
横向无缝,所以斜划时手指只要有一点弧度就会蹭进隔壁键 —— 而人划不出直线。拿真机坐标做模拟(弧高 5%):
| 路径 | 整键判定(旧) | 中央 56%(现在) |
|---|---|---|
| 1→5 | 145 / 415 |
15 |
| 5→9 | 589 |
59 |
| 7→5 | 785 |
75 |
| 一笔 1-5-9-7-5-3 | 14589878563 |
1598753 |
所以命中判定只认键中央 56% 那一块:真的「划进去了」才算,蹭边角不算。起点键仍然用整个键判定 —— 人可能按在数字边上,那一按系统会认。
二、同一个键被上报两次
手指停在中央那块的边界上会来回穿越,同一个键就会被上报两次。真机日志里抓到过间隔 5ms 的重复上报,人不可能是这个速度:
手势 1 4(+75ms) 5(+149ms) 5(+5ms) ← 多了一个 5
规则是:连续两次上报同一个键就吞掉。中间隔了别的键(真的划回来)照报,所以 1→5→1 依然是 151。
顺带一个诚实的说明:有些密码必须分两笔
159753 这种图案,9→7 是横穿,必然经过 8。而 5 在 1→5→9 里同样是「一条直线穿过的中间那个」—— 两种情况几何上完全一样,任何算法都分不出来,Flyme 也一样,它没有读心术。
所以实际是两笔:
1 → 5 → 9 (一条笔直的斜线)
抬手
7 → 5 → 3 (另一条平行的斜线)
实测日志,两笔合起来正好 159753,而且比一笔更顺手:
23:33:39 1 5(+75ms) 9(+243ms) [抬手 705ms] 7 5(+33ms) 3(+239ms) -> 159753
想「一笔划完、中间穿过的不算」也能做 —— 但那会让直线划 1→2→3 只出 1、3,两者互斥,只能选一个。
装起来,先验证再上锁屏
- 装 APK(普通安装即可,它同时是个可打开的演示页)
- LSPosed 里启用 PIN 滑动输入,作用域勾 SystemUI(APK 里已用
xposedscope声明) - 重启 SystemUI 才会注入:
su -c 'killall com.android.systemui'
先在演示页试手感。 APK 里带一个和锁屏同样布局的假键盘,用的是同一个 SwipePadController,挂点也一样。在这个页面上试:按住一位数字别松手一路划过去;再单点几下确认不会翻倍。手感对了再去锁屏。
怎么确认生效:模块只在 hook / 接管 / 出错时写 LSPosed 日志:
su -c 'grep PinSwipe /data/adb/lspd/log/modules_*.log | tail -20'
PinSwipe: hooked com.android.keyguard.NumPadKey#onTouchEvent
PinSwipe: 接管 PIN 键盘,数字键 10 个,附加键 1 个
逐位日志默认关闭(LOG_TRIGGERS = false),要核对手感就改成 true 重新装。
出事了怎么退
模块只做加法、绝不吞事件,最坏情况是「滑动没反应」或「多输一位」,不会让你点不动。
- KernelSU Next 的 safe mode(开机时按住音量减)
- 进 recovery 删掉
/data/adb/modules/zygisk_lsposed - 或者电脑上
adb shell su -c 'killall com.android.systemui'后在 LSPosed 里关掉模块
⚠️ 输错密码会计入失败次数,测试时别拿一整排数字去划,划两位就够了。
没做的
- 只覆盖 PIN(数字密码)。图形密码 / 混合密码没做 —— 那两种的「划过输入」语义不清晰。
- 停留在 ⌫ 上清空一次,不会连续重复清空。
- 一笔划完「中间穿过别的数字」的图案,做不到,那是几何问题不是 bug。
- 长按数字键的原有行为不受影响(只补 MOVE,不碰 DOWN/UP)。
最新版本 V1.2(演示页加了「清除模块日志」和「关于」),在 Releases 里下。
作者:Deepseek · B站 @z7ui