在Android上实现手势密码

看到这个标题你可能会笑,Android手势密码的文章打印出来都可以绕地球好几圈了,还有什么好说的?当然有。绝大部分的文章都是写怎么实现9点手势密码,有个重要的地方,都没涉及到,那就是怎样触发手势密码。

You will think this post as a joke, as there are so many posts can be found by Google. What makes this post special? Exactly. Most of the other posts show you how to create 9-point gesture view, but miss a very important point about how to trigger it.

我们要实现的需求如下:(前提是已经设置并保持了手势密码)

We are going to implement these requirements: (Gesture password has be set for premise)

  1. 点击桌面图标启动App,先展示Splash页面,然后弹出验证手势密码的页面,输入完成后打开App。
  2. 应用启动状态,用户按Home键使App进入后台,或者切换到其他应用使本应用进入后台,或者锁屏。这几种情况用户都不会再操作这个App,超过一段时间之后再打开App,弹出验证手势密码的页面,输入正确进入之前的页面。(如果时间过长导致应用被杀死,应该属于第一点需求)

  3. Launch App by icon in launcher, show splash page and popup gesture page later. Open home page when verify gesture successfully.

  4. When our App is in foreground, user press home button and App was moved backwards or move other Apps to foreground or turn off screen. For these conditions our user would re open or active our App at some time after, maybe a long or short time, and we want our user verify gesture password first before they enter our App, for security. (If that is a long time and our App was killed by OS, see condition 1.)
    第一个需求相对简单,大家都知道怎么做,难的是第二个。不要急,听我说。

The first one is as simple as everyone knows how to deal with. I’ll talk about the other one next.

首先,你的项目里需要一个BaseActivity,这是不可或缺的。当然,不仅仅是这个需求需要,很多需求都要求有一个BaseActivity,设计BaseActivity也是个好习惯。

First of all, there need a BaseActivity in your project, indispensably. For more, this is a very good practice in development, not just for this requirement.

然后,核心逻辑要放在Activity进入后台和从后台恢复的生命周期回调中。网上有帖子说放onStop和onStart中,这是不可行的,在有Activity切换动画的情况下,前一个Activity的onStop是在另一个Activity的onStart之后才调用的,这个逻辑使得这两个方法不适合作为逻辑的入口。我们放到onResume和onPause中。现在,你的BaseActivity应该长这样。

Then, the core logic goes in callbacks of Activity. There are some posts said onStop and onStart where it is but I say no. If there were transition animations, onStart of next Activity was invoked before onStop of the first one. We don’t want this. We are going to put our logic in this brother: onPause and onResume. Now, our BaseActivity should looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package com.kyleduo.gesturepass;

import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

/**
* BaseActivity
* Created by kyle on 16/4/6.
*/
public class BaseActivity extends AppCompatActivity {
private static final int BACK_TIME_INITIAL = 0; // 初始值,遇到0必须检查手势密码时间
private static final int BACK_TIME_CLEAR = -1; // 不需要检查手势密码时间

public static final String KEY_FROM_PUSH = "from_push";
public static final int GESTURE_PASS_TIMEOUT = 2 * 1000;

protected boolean mFromPush;

protected static long sLastBackTime = BACK_TIME_INITIAL; // 0: 初始值 -1: 当前在前台

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mFromPush = getIntent().getBooleanExtra(KEY_FROM_PUSH, false);
Log.d(tag(), "from push: " + mFromPush);
boolean fromHistory = (getIntent().getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0;
Log.d(tag(), "fromHistory: " + fromHistory + " sLastBackTime: " + sLastBackTime);
if (sLastBackTime != BACK_TIME_INITIAL && shouldCheckGesturePass() && !isFromPush() && fromHistory) {
sLastBackTime = BACK_TIME_CLEAR;
Log.e(tag(), "onCreate >>>>>>>>>>>>>>>> reset sLastBackTime <<<<<<<<<<<<<<<");
}
}

private String tag() {
return getClass().getName();
}

public boolean isFromPush() {
return mFromPush;
}

@Override
protected void onResume() {
super.onResume();
if (shouldCheckGesturePass()) {
//noinspection ConstantConditions
if (checkGestureTime() && gesturePassIsAvailable()) {
Log.d(tag(), "show gesture pass");
Intent intent = new Intent(this, GesturePassActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
overridePendingTransition(0, 0);
sLastBackTime = BACK_TIME_CLEAR;
}
} else {
sLastBackTime = BACK_TIME_CLEAR;
Log.d(tag(), "onResume >>>>>>>>>>>>>>>> reset sLastBackTime <<<<<<<<<<<<<<<");
}
}

/**
* 检查时间是否满足手势密码的间隔
*
* @return true 时间满足
*/
protected boolean checkGestureTime() {
Log.d(tag(), "checkGestureTime: sLastBackTime: " + sLastBackTime + " isPush: " + isFromPush());
double diff = Math.abs(SystemClock.elapsedRealtime() - sLastBackTime);
boolean timeSatisfy = diff > GESTURE_PASS_TIMEOUT;
Log.d(tag(), "checkGestureTime: diff: " + diff);
return sLastBackTime != BACK_TIME_CLEAR && timeSatisfy;
}

@Override
protected void onPause() {
super.onPause();
if (shouldCheckGesturePass()) {
if (this instanceof GesturePassActivity) {
sLastBackTime = BACK_TIME_CLEAR;
} else {
sLastBackTime = SystemClock.elapsedRealtime();
}
Log.e(tag(), "onPause: sLastBackTime: " + sLastBackTime);
}
}

/**
* 测试方法,始终返回true
*
* @return true
*/
private boolean gesturePassIsAvailable() {
return true;
}

protected boolean shouldCheckGesturePass() {
return true;
}
}

最基本的逻辑是,(L1)给BaseActivity定义一个静态成员变量sLastBackTime ,在onPause中保存时间戳,在onResume中取这个时间戳和当前时间对比,如果超过了手势密码延迟的阈值(TH),就打开手势密码页面。

The basic logic is to declare a static variable sLastBackTime to store a timestamp indicate the time when onPause was invoked. And when onResume invoked, compare the difference of sLastBackTime to the time of ‘now’. If the difference over the threshold before gesture password, then we open the gesture password page.

实际情况会更复杂一些,比如要考虑到对手势密码页面和推送的特殊处理。如果有的页面不需要处理手势密码,就覆写shouldCheckGesturePass 方法,返回false就可以了。是不是还蛮简单的?

It is more complicated for the reality, such as how to deal with the gesture password page and push notifications. For pages that not need to condition gesture pass, just override shouldCheckGesturePass method and return false. Is it clear?

Demo源码可以在Github上找到。

Visit Github for source code of demo.