手势解锁:九宫格手势绘制、密码存储、验证逻辑
手势解锁,说白了就是那个你每天划几百次的九宫格锁屏。嗯,这功能看起来简单,但真要自己实现一遍,坑还挺多的。我记得第一次做这个功能时,以为就是画几条线的事,结果被触摸事件的分发、密码的加密存储、还有那个烦人的「最少连接4个点」的校验,折腾了好几天。
今天我们就来拆解一下,一个完整的手势解锁模块,到底该怎么设计。
核心流程:从触摸到验证
先看整体流程,我习惯用一张图来理清思路:
整个流程其实就三步:触摸绘制 → 路径校验 → 存储/验证。但每一步都有细节,我们一个一个说。
九宫格绘制:自定义View的必修课
手势解锁的核心,是一个自定义View。我建议你直接继承 View,而不是 ImageView 或别的。为什么?因为我们需要完全控制绘制逻辑,包括触摸事件的处理。
1. 定义九宫格的点位
九宫格有9个点,3行3列。每个点需要记录:
- 坐标位置(x, y)
- 当前状态(未选中、选中、错误)
- 对应的数字(1-9,用于生成密码序列)
class Point {
float x, y;
int index; // 1-9
State state; // NORMAL, SELECTED, ERROR
enum State { NORMAL, SELECTED, ERROR }
}
初始化时,根据View的宽高,计算出每个点的中心坐标。嗯,这里要注意:
2. 绘制状态
绘制分三层:
- 底圆:未选中时灰色空心圆,选中时蓝色实心圆
- 连线:按用户触摸顺序,在选中的点之间画线
- 箭头指示:在连线末端画一个小箭头,提示方向(这个看产品需求,不是必须的)
我个人习惯把绘制逻辑拆成三个方法:
@Override
protected void onDraw(Canvas canvas) {
drawPoints(canvas); // 绘制所有点
drawLines(canvas); // 绘制连线
drawFinger(canvas); // 绘制手指当前位置的指示线
}
这样代码清晰,后期改样式也方便。你想想看,如果所有绘制都塞在一个方法里,改起来多痛苦。
触摸事件处理:别让手指「溜走」
手势解锁的触摸逻辑,比普通点击复杂。因为用户可能从点1滑到点5,中间经过点2、点3。我们需要实时判断手指当前落在哪个点上。
核心逻辑:
- ACTION_DOWN:判断手指是否落在某个点范围内,如果是,记录为第一个选中点
- ACTION_MOVE:每次移动都检查当前坐标是否落在某个未选中的点内,如果是,加入选中列表
- ACTION_UP:结束绘制,生成密码序列,触发验证
这里有个细节:如果用户手指从点1滑到点3,中间经过了点2,但手指没有在点2上停留。要不要自动选中点2?
我的做法是:不自动选中。因为用户可能想画一个「1→3」的斜线,中间的点2只是路过。如果自动选中,反而会干扰用户意图。当然,有些产品要求「路过即选中」,那就需要做路径插值计算了。
密码存储:别存明文!
手势密码本质上是一个数字序列,比如「1-5-9-8-7」。但直接存这个序列?绝对不行。
我见过一些App,把手势密码用SharedPreferences明文存储。嗯,这跟把银行卡密码写在便利贴上贴手机背面,没什么区别。
正确的做法:
- 使用 SHA-256 或更安全的哈希算法,对密码序列进行哈希
- 存储时加盐(Salt),防止彩虹表攻击
- 存储在 EncryptedSharedPreferences 或 KeyStore 中
fun saveGesturePassword(password: String) {
val salt = generateSalt() // 随机生成盐值
val hash = hashPassword(password, salt) // SHA-256 哈希
// 存储 hash 和 salt
encryptedPrefs.edit()
.putString("gesture_hash", hash)
.putString("gesture_salt", salt)
.apply()
}
验证逻辑:不只是比对字符串
验证时,用户重新绘制一遍手势,生成密码序列,然后与存储的哈希值比对。
但这里有个问题:用户绘制的路径,可能因为手指滑动速度、角度不同,导致生成的序列顺序有细微差异吗?
不会。因为我们是按「选中点的顺序」生成序列的,只要用户依次划过相同的点,序列就是固定的。所以直接比对哈希值即可。
验证流程:
- 用户绘制完成,生成密码序列(如 "1-5-9-8-7")
- 从存储中取出 salt 和 hash
- 对用户输入的序列 + salt 进行哈希
- 比对两个哈希值是否相等
fun verifyGesture(password: String): Boolean {
val storedHash = encryptedPrefs.getString("gesture_hash", "")
val storedSalt = encryptedPrefs.getString("gesture_salt", "")
val inputHash = hashPassword(password, storedSalt)
return inputHash == storedHash
}
你看,逻辑其实很简单。但实际项目中,我还会加一个「最小连接数」的校验。比如至少连接4个点,否则提示「密码太短,请重新绘制」。
避坑指南:那些年我踩过的雷
1. 触摸事件被父容器拦截
如果手势解锁View放在ScrollView或ViewPager里,父容器可能会拦截触摸事件。解决办法:在View的onTouchEvent中调用 getParent().requestDisallowInterceptTouchEvent(true)。
2. 密码序列的「回退」问题
用户画错了,想撤销上一个点。有些产品支持「手指滑回上一个点,取消选中」。这个实现起来比较麻烦,需要记录路径历史,并支持反向操作。我个人建议:如果产品没有明确要求,就别加这个功能,容易把逻辑搞复杂。
3. 屏幕旋转与Activity重建
手势解锁通常在全屏Activity中展示。如果用户旋转屏幕,Activity重建,绘制状态会丢失。解决办法:在onSaveInstanceState中保存选中的点列表,在onRestoreInstanceState中恢复。
性能优化:别让绘制卡顿
手势解锁的绘制频率很高——手指每移动一点,都要重绘。如果onDraw里做了耗时操作,界面就会卡顿。
我建议:
- 不要在onDraw里创建对象(比如new Paint()、new Path()),全部在初始化时创建好
- 使用
invalidate()而不是postInvalidate(),除非你在非UI线程 - 如果点很多(比如4x4、5x5的网格),考虑使用
SurfaceView代替普通View
好了,关于九宫格手势解锁的核心实现,就聊到这里。代码示例都是基于我实际项目中的经验提炼的,你可以直接拿去用。如果有细节问题,欢迎随时交流。
公众号:蓝海资料掘金营,微信deep3321