Android 自定义 View 实现手势解锁详解
背景与需求
在移动应用开发中,安全性是至关重要的考量因素。为了提升用户体验并保障账户安全,许多金融类或隐私类应用(如支付宝)采用了手势密码作为二次验证手段。当用户切出应用一段时间后返回,系统会要求通过绘制特定轨迹来解锁。本文档将详细介绍如何在 Android 平台上通过自定义 View 实现这一功能,包括核心逻辑分析、代码实现及集成步骤。
核心思路
实现手势解锁主要涉及以下三个关键点:
- 自定义 View:继承
android.view.View,重写onMeasure、onLayout和onDraw方法,负责绘制 3x3 的九宫格圆圈。 - 触摸事件处理:重写
onTouchEvent,监听用户的点击、滑动和抬起动作,计算触摸点所在的圆圈,并记录路径。 - 状态管理与绘制:根据当前操作状态(正常、触摸中、错误)切换画笔颜色和样式,动态刷新界面。
项目布局配置
首先,在布局文件中引入自定义 View。假设自定义 View 的包名为 com.example.gesturelock。
<com.example.gesturelock.MyGestureLockView
android:id="@+id/gesture_lock_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
核心代码实现
1. 辅助类 MyCycle
该类用于表示九宫格中的每一个圆圈节点,存储坐标、半径、编号及选中状态。
package com.example.gesturelock;
public class MyCycle {
private int ox; // 圆心横坐标
private int oy; // 圆心纵坐标
private float r; // 半径大小
private Integer num; // 节点编号
private boolean onTouch; // false=未选中
public int getOx() {
return ox;
}
public void setOx(int ox) {
this.ox = ox;
}
public int getOy() {
return oy;
}
public void setOy(int oy) {
this.oy = oy;
}
public float getR() {
return r;
}
public void setR(float r) {
this.r = r;
}
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
public boolean isOnTouch() {
return onTouch;
}
public void setOnTouch(boolean onTouch) {
this.onTouch = onTouch;
}
// 判断点是否在圆内
public boolean isPointIn(int x, int y) {
double distance = Math.sqrt((x - ox) * (x - ox) + (y - oy) * (y - oy));
return distance < r;
}
}
2. 主视图类 MyGestureLockView
这是实现手势锁的核心类,包含了绘制逻辑和交互逻辑。
package com.example.gesturelock;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class MyGestureLockView extends View {
/**
* 不同状态的画笔
*/
private Paint paintNormal;
private Paint paintOnTouch;
private Paint paintInnerCycle;
private Paint paintLines;
private Paint paintKeyError;
private MyCycle[] cycles;
private Path linePath = new Path();
private List<Integer> linedCycles = new ArrayList<>();
private OnGestureFinishListener onGestureFinishListener;
private String key;
private int eventX, eventY;
private boolean canContinue = true;
private boolean result;
private Timer timer;
/**
* 不同状态下的色值
*/
private int OUT_CYCLE_NORMAL = Color.rgb(108, 119, 138); // 外圈正常色
private int OUT_CYCLE_ONTOUCH = Color.rgb(25, 66, 103); // 选中外圈色
private int INNER_CYCLE_ONTOUCH = Color.rgb(2, 210, 255); // 选中内圆色
private int LINE_COLOR = Color.argb(127, 2, 210, 255); // 连线颜色
private int ERROR_COLOR = Color.argb(127, 255, 0, 0);
public void setOnGestureFinishListener(OnGestureFinishListener onGestureFinishListener) {
this.onGestureFinishListener = onGestureFinishListener;
}
public void setKey(String key) {
this.key = key;
}
public MyGestureLockView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public MyGestureLockView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyGestureLockView(Context context) {
super(context);
init();
}
private void init() {
paintNormal = new Paint();
paintNormal.setAntiAlias(true);
paintNormal.setStrokeWidth(3);
paintNormal.setStyle(Paint.Style.STROKE);
paintOnTouch = new Paint();
paintOnTouch.setAntiAlias(true);
paintOnTouch.setStrokeWidth(3);
paintOnTouch.setStyle(Paint.Style.STROKE);
paintInnerCycle = new Paint();
paintInnerCycle.setAntiAlias(true);
paintInnerCycle.setStyle(Paint.Style.FILL);
paintLines = new Paint();
paintLines.setAntiAlias(true);
paintLines.setStyle(Paint.Style.STROKE);
paintLines.setStrokeWidth(6);
paintKeyError = new Paint();
paintKeyError.setAntiAlias(true);
paintKeyError.setStyle(Paint.Style.STROKE);
paintKeyError.setStrokeWidth(3);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
int perSize = 0;
if (cycles == null && (perSize = getWidth() / 6) > 0) {
cycles = new MyCycle[9];
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
MyCycle cycle = new MyCycle();
cycle.setNum(i * 3 + j);
cycle.setOx(perSize * (j * 2 + 1));
cycle.setOy(perSize * (i * 2 + 1));
cycle.setR(perSize * 0.5f);
cycles[i * 3 + j] = cycle;
}
}
}
}
/**
* 绘制所需要绘制的内容
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < cycles.length; i++) {
if (!canContinue && !result) {
paintOnTouch.setColor(ERROR_COLOR);
paintInnerCycle.setColor(ERROR_COLOR);
paintLines.setColor(ERROR_COLOR);
} else if (cycles[i].isOnTouch()) {
paintOnTouch.setColor(OUT_CYCLE_ONTOUCH);
paintInnerCycle.setColor(INNER_CYCLE_ONTOUCH);
paintLines.setColor(LINE_COLOR);
} else {
paintNormal.setColor(OUT_CYCLE_NORMAL);
paintInnerCycle.setColor(INNER_CYCLE_ONTOUCH);
paintLines.setColor(LINE_COLOR);
}
if (cycles[i].isOnTouch()) {
canvas.drawCircle(cycles[i].getOx(), cycles[i].getOy(),
cycles[i].getR(), paintOnTouch);
drawInnerBuleCycle(cycles[i], canvas);
} else {
canvas.drawCircle(cycles[i].getOx(), cycles[i].getOy(),
cycles[i].getR(), paintNormal);
}
}
drawLine(canvas);
}
/**
* 绘制大圆里的小圆
*/
private void drawInnerBuleCycle(MyCycle cycle, Canvas canvas) {
canvas.drawCircle(cycle.getOx(), cycle.getOy(), cycle.getR() / 3,
paintInnerCycle);
}
private void drawLine(Canvas canvas) {
linePath.reset();
if (linedCycles.size() > 0) {
for (int i = 0; i < linedCycles.size(); i++) {
int index = linedCycles.get(i);
if (i == 0) {
// 设置为整条路径的起点
linePath.moveTo(cycles[index].getOx(), cycles[index].getOy());
} else {
linePath.lineTo(cycles[i].getOx(), cycles[i].getOy());
}
}
linePath.lineTo(eventX, eventY);
canvas.drawPath(linePath, paintLines);
}
}
/**
* 根据手势时触摸点的不同,修改对应的状态值
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (canContinue) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
eventX = (int) event.getX();
eventY = (int) event.getY();
for (int i = 0; i < cycles.length; i++) {
if (cycles[i].isPointIn(eventX, eventY)) {
cycles[i].setOnTouch(true);
if (!linedCycles.contains(cycles[i].getNum())) {
linedCycles.add(cycles[i].getNum());
}
}
}
break;
case MotionEvent.ACTION_UP:
canContinue = false;
StringBuffer sb = new StringBuffer();
for (int i = 0; i < linedCycles.size(); i++) {
sb.append(linedCycles.get(i));
}
result = key.equals(sb.toString());
if (onGestureFinishListener != null) {
onGestureFinishListener.OnGestureFinish(result);
}
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// 回到初始状态
eventX = eventY = 0;
for (int i = 0; i < cycles.length; i++) {
cycles[i].setOnTouch(false);
}
linedCycles.clear();
linePath.reset();
canContinue = true;
postInvalidate();
}
}, 1000);
break;
}
}
invalidate();
return true;
}
}
3. 接口定义
为了接收手势结果,需要定义一个回调接口。
package com.example.gesturelock;
public interface OnGestureFinishListener {
void OnGestureFinish(boolean result);
}
使用示例
在 Activity 中初始化并设置密码。
MyGestureLockView lockView = findViewById(R.id.gesture_lock_view);
lockView.setKey("012"); // 设置正确密码为 0->1->2
lockView.setOnGestureFinishListener(new OnGestureFinishListener() {
@Override
public void OnGestureFinish(boolean result) {
if (result) {
// 解锁成功,跳转页面
} else {
// 解锁失败,提示重试
}
}
});
常见问题与优化建议
- 坐标适配:上述代码基于 View 的宽度进行简单均分。在实际项目中,建议使用
dp单位或更复杂的测量逻辑来适配不同屏幕密度。 - 动画效果:当前实现为静态绘制。可添加
ValueAnimator使圆圈选中时有缩放动画,提升交互体验。 - 安全性:手势密码仅作为本地 UI 层验证。敏感数据不应仅依赖此方式保护,后端应配合 Token 或生物识别机制。
- 性能优化:避免在
onDraw中频繁创建对象,画笔和路径应在初始化时复用。
总结
通过自定义 View 和触摸事件监听,我们可以灵活地实现各种图形化交互界面。手势解锁是一个经典的 Android 面试题,掌握其原理有助于深入理解 Android 绘图系统和事件分发机制。开发者可根据实际需求调整颜色、网格数量或增加动画效果。

