跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Java大前端java

Android 自定义 View 实现手势解锁详解

Android 自定义 View 实现手势解锁功能,通过继承 View 类并重写 onDraw 与 onTouchEvent 方法完成九宫格绘制与触摸轨迹追踪。核心逻辑包含 MyCycle 节点管理、Paint 画笔状态切换及路径绘制。文章提供完整 Java 代码示例,涵盖布局配置、接口回调及 Activity 集成方案,解决了手势轨迹记录、状态重置及视觉反馈等关键问题,适用于金融类应用的二次验证场景。

监控大屏发布于 2025/2/7更新于 2026/10/1684 浏览
Android 自定义 View 实现手势解锁详解

Android 自定义 View 实现手势解锁详解

背景与需求

在移动应用开发中,安全性是至关重要的考量因素。为了提升用户体验并保障账户安全,许多金融类或隐私类应用(如支付宝)采用了手势密码作为二次验证手段。当用户切出应用一段时间后返回,系统会要求通过绘制特定轨迹来解锁。本文档将详细介绍如何在 Android 平台上通过自定义 View 实现这一功能,包括核心逻辑分析、代码实现及集成步骤。

核心思路

实现手势解锁主要涉及以下三个关键点:

  1. 自定义 View:继承 android.view.View,重写 onMeasure、onLayout 和 onDraw 方法,负责绘制 3x3 的九宫格圆圈。
  2. 触摸事件处理:重写 onTouchEvent,监听用户的点击、滑动和抬起动作,计算触摸点所在的圆圈,并记录路径。
  3. 状态管理与绘制:根据当前操作状态(正常、触摸中、错误)切换画笔颜色和样式,动态刷新界面。

项目布局配置

首先,在布局文件中引入自定义 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 {
            // 解锁失败,提示重试
        }
    }
});

常见问题与优化建议

  1. 坐标适配:上述代码基于 View 的宽度进行简单均分。在实际项目中,建议使用 dp 单位或更复杂的测量逻辑来适配不同屏幕密度。
  2. 动画效果:当前实现为静态绘制。可添加 ValueAnimator 使圆圈选中时有缩放动画,提升交互体验。
  3. 安全性:手势密码仅作为本地 UI 层验证。敏感数据不应仅依赖此方式保护,后端应配合 Token 或生物识别机制。
  4. 性能优化:避免在 onDraw 中频繁创建对象,画笔和路径应在初始化时复用。

总结

通过自定义 View 和触摸事件监听,我们可以灵活地实现各种图形化交互界面。手势解锁是一个经典的 Android 面试题,掌握其原理有助于深入理解 Android 绘图系统和事件分发机制。开发者可根据实际需求调整颜色、网格数量或增加动画效果。

目录

  1. Android 自定义 View 实现手势解锁详解
  2. 背景与需求
  3. 核心思路
  4. 项目布局配置
  5. 核心代码实现
  6. 1. 辅助类 MyCycle
  7. 2. 主视图类 MyGestureLockView
  8. 3. 接口定义
  9. 使用示例
  10. 常见问题与优化建议
  11. 总结

更多推荐文章

查看全部
  • MyLesson 微信小程序前台开发实战(一)
  • VS Code 安装 GitHub Copilot 实现 AI 辅助编程指南
  • Qwen3-4B-Instruct 提示词优化与使用避坑指南
  • Stable Diffusion WebUI 云端部署与实战指南
  • 个人开发者合法使用 JetBrains 的几种途径
  • Agent 和 Token 的区别:概念、关系与代码实现
  • OpenAI Whisper 语音识别工具本地部署与使用
  • Claude Code 与 GitHub Copilot CLI 终端选型实测
  • CKS 核心命令速查指南
  • 基于飞算 JavaAI 的智能在线考试系统设计与实现
  • AI猫娘?让微信接入Deepseek:获得一个AI聊天机器人,喵~
  • Joplin 开源笔记应用核心功能解析与跨平台同步指南
  • 垂直微调大模型与通用大模型在情感场景下的能力对比分析
  • Java 大数据在新能源微电网能量优化调度与虚拟电厂协同控制中的应用实践
  • STM32 摄像头颜色识别实战:OV7725/OV2640 驱动与算法解析
  • Arduino BLDC 自主巡逻机器人设计与实现(避障 + 路径规划)
  • voidImageViewer:轻量级图像查看工具,支持 GIF/WEBP 动画
  • 发型设计 APP:基于 GLM-4.6V-Flash-WEB 的脸型适配剪发推荐
  • 学习 Python 推荐的 10 个优质技术资源网站
  • 数据结构常见排序算法汇总:插入、交换、选择与归并

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online