本文基于Hongyang大神的博客:http://blog.csdn.net/lmj623565791/article/details/36236113
转载请注明来源:http://blog.csdn.net/u013258802/article/details/53079019
界面和样式的调整参考前三篇,本文目的是简单介绍下手势密码的具体应用。
每个APP的设计都不同,支付宝是在主页的“我的”tab页显示的时候唤起手势密码,QQ是任何页面从后台进入显示时都会唤起,还有些APP是退到后台一定时间之内(例如两分钟)不唤起手势,第一种很好做就不说了,这篇文章讲后两种设计。
要实现全APP内手势密码验证,主要考虑两种情况:
1、刚打开APP或者从后台切入或者息屏,需要手势密码验证;
2、APP内界面跳转过程,不需要手势密码验证。
Activity的生命周期就不提了,上述两种情况都会走到onResume()方法并检测是否开启了手势密码验证,关键在于该过程是否是APP内部的页面切换过程,是的话onResume()中就需要跳过手势检查,理清了思路,问题就好解决了。
略过闲杂方法,Activity A 到 B 的切换过程如下:
A:onPause();
B:onResume();
A:onStop()。
可见关键点就在这三个方法中,我们从带时间的手势唤起方案说起。
一、APP退到后台超过一定时间(暂定60秒)后唤起手势:
需要一个全局变量 lockTime 贯穿整个APP使用过程,该变量在启动APP时初始化为0,退出APP时置0;
在 BaseActivity 的 onResume() 方法中取当前时间 sysTime - lockTime,获得差值 durTime;
刚打开APP时 durTime 必定大于 60*1000ms,一定会显示手势密码;
在 onPause() 方法中保存当前系统时间 lockTime = sysTime;
下一次调用 onResume() 时会再次判断间隔时间 durTime,页面跳转时间必定小于 60s,不显示手势密码。
根据以上设计我们可以大致确定要做的事和需要的类:
(1)一个处处能用的时间相关的值,本文选择了在Application中配置全局变量,当然用SharedPreference随存随取也行;
(2)一个基类,重写onResume()和onPause(),负责取值和写值,取值后判断是否显示手势页面;
(3)一个手势页面,根据手势设置进行相关显示处理;
(4)一个保存手势设置的类。
先新建一个项目,准备好自定义的Application、BaseActivity等:
在自定义的 Application 中添加 lockTime 变量和对应的set/get方法:
public class MyApp extends Application {
private static MyApp instance = null;
public static MyApp getInstance() {
return instance;
}
@Override
public void onCreate() {
super.onCreate();
instance = this;
}
/** ----------------------- 一些公共的变量 ------------------------- */
private long lockTime = 0; // 保存的是最近一次调用onPause()的系统时间
private Setting settings; // 手势设置
/** ----------------------- 一些set/get方法 ------------------------- */
public long getLockTime() {
return lockTime;
}
public void setLockTime(long lockTime) {
this.lockTime = lockTime;
}
public Setting getSettings() {
return settings;
}
public void setSettings(Setting settings) {
this.settings = settings;
}
}
Setting 类只有两个字段,也可以附带错误次数,很明显这些变量的值将进行保存:
/**
* 保存手势密码相关设置
*/
public class Setting {
private String gesture; // 手势密码
private String showPath;// 是否显示轨迹
public Setting(String gesture, String showPath) {
this.gesture = gesture;
this.showPath = showPath;
}
// get()和set()
...
}
重写 BaseActivity 的 onResume() 和 onPause() 方法:
public class BaseActivity extends AppCompatActivity {
private static final String TAG = BaseActivity.class.getSimpleName();
private Context mContext;
private MyApp myApp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this;
myApp = MyApp.getInstance();
}
@Override
protected void onResume() {
super.onResume();
long durTime = System.currentTimeMillis() - myApp.getLockTime();
if (durTime > 60 * 1000) {
if (myApp.getSettings() != null) {
if (myApp.getSettings().getGesture() != null
&& !myApp.getSettings().getGesture().isEmpty()) {
startActivity(new Intent(mContext, LockActivity.class));
}
}
}
}
@Override
protected void onPause() {
super.onPause();
myApp.setLockTime(System.currentTimeMillis());
}
}
LockActivity就是我们要弹出的手势界面:
public class LockActivity extends BaseActivity {
private static final String TAG = LockActivity.class.getSimpleName();
private Context mContext;
private MyApp myApp;
private TextView mTextView;
private GestureLockViewGroup mGesture;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_lock);
mContext = this;
myApp = MyApp.getInstance();
initView();
}
private void initView() {
mTextView = (TextView) findViewById(R.id.tv_prompt_lock);
mTextView.setText("请绘制手势密码");
mGesture = (GestureLockViewGroup) findViewById(R.id.gesture_lock_view_group_lock);
mGesture.setAnswer(myApp.getSettings().getGesture());
mGesture.setShowPath(Setting.SHOW_PATH.equals(myApp.getSettings().getShowPath()));
mGesture.setOnGestureLockViewListener(new GestureLockViewGroup.OnGestureLockViewListener() {
@Override
public void onBlockSelected(int cId) {
}
@Override
public void onGestureEvent(boolean matched) {
if (matched) {
mTextView.setText("输入正确");
finish();
} else {
mTextView.setText("手势错误,还剩"+ mGesture.getTryTimes() + "次");
}
}
@Override
public void onUnmatchedExceedBoundary() {
// 正常情况这里需要做处理(如退出或重登)
Toast.makeText(mContext, "错误次数太多,请重新登录", Toast.LENGTH_SHORT).show();
}
@Override
public void onFirstSetPattern(boolean patternOk) {
}
});
}
}
LockActivity的布局放个GestureLockViewGroup控件加TextView做提示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:zhy="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="gesture.test.liao.gesturelock.activity.LockActivity">
<ImageView
android:contentDescription="@string/app_name"
android:layout_gravity="center"
android:src="@mipmap/ic_launcher"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_prompt_lock"
tools:text="hhh"
android:gravity="center"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<gesture.test.liao.gesturelock.view.GestureLockViewGroup
android:id="@+id/gesture_lock_view_group_lock"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#00ffffff"
android:gravity="center_vertical"
zhy:count="3"
zhy:tryTimes="5"
zhy:color_no_finger_inner_circle="#00000000"
zhy:color_no_finger_outer_circle="#ff3595ff"
zhy:color_finger_on="#ff3595ff" />
</LinearLayout>
配置完成后,在启动页或者主页面读取手势设置即可:
MyApp.getInstance().setSettings(loadSettings());
/**
* 读取手势设置
* @return
*/
private Setting loadSettings() {
Setting setting = null;
try {
ObjectInputStream in = new ObjectInputStream(mContext.openFileInput("setting.txt"));
setting = (Setting) in.readObject();
in.close();
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return setting;
}
当然在设置页面我们需要针对当前设置进行存储:
/**
* 存手势设置
*/
private void saveSettings(Setting setting) {
try {
ObjectOutputStream out = new ObjectOutputStream(
mContext.openFileOutput("setting.txt", MODE_PRIVATE));
out.writeObject(setting);
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
主页的onDestroy()方法中需要将时间变量置0:
@Override
protected void onDestroy() {
super.onDestroy();
// 不置零的效果是,如果在设定的一分钟之内再次打开APP则不会弹出手势密码
// 因为应用的Activity全部finish后Application可能还存在
// 这句置零代码也可以放在启动APP页面onResume()方法之前
MyApp.getInstance().setLockTime(0);
}
当然这样做的话还存在许多问题,例如启动页、登录页等继承自基类就会弹手势密码页,LockActivity页面停留时间过长也会再次显示手势密码页面,LockActivity的返回事件也需要处理,要达到Lock页面按返回键所有Activity都finnish或者退到后台。
改进:
我们给BaseActivity一个字段,控制手势密码的开启与关闭,在特殊页面调用disablePatternLock()方法禁用手势密码,再传参数决定下一个页面是否能唤起手势页面
BaseActivity:
public class BaseActivity extends AppCompatActivity {
private static final String TAG = BaseActivity.class.getSimpleName();
private Context mContext;
private MyApp myApp;
// 页面是否允许唤起手势密码
private boolean enableLock = true;
// 下一个页面是否唤起手势密码
private boolean nextShowLock = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this;
myApp = MyApp.getInstance();
}
@Override
protected void onResume() {
super.onResume();
if (enableLock) {
// 减得当前APP在后台滞留的时间 durTime
long durTime = System.currentTimeMillis() - myApp.getLockTime();
if (durTime > 60 * 1000) {
// 显示手势密码页面
showLockActivity();
}
}
}
@Override
protected void onPause() {
super.onPause();
if (enableLock || !nextShowLock) {
// 更新 lockTime
myApp.setLockTime(System.currentTimeMillis());
}
}
/**
* 跳转至手势密码页面
*/
private void showLockActivity() {
if (myApp.getSettings() != null
&& myApp.getSettings().getGesture() != null
&& !myApp.getSettings().getGesture().isEmpty()) {
startActivity(new Intent(mContext, LockActivity.class));
}
}
/**
* 部分页面禁用手势密码需要调用该方法,例如启动页、注册登录页、解锁页(LockActivity)等
* 在这些页面如果停留时间较久后,如果想进入下一个页面时不弹出手势,需要在finish前手动添加
* myApp.setLockTime(System.currentTimeMillis());
* 或者传入新的参数进行标识,在onPause中根据标识判断是否setLockTime
* 本例选择传入参数
* nextShowLock 为false 表示onPause()会调用setLockTime(),则下一个页面不会唤起手势
*
* @param nextShowLock
*/
protected void disablePatternLock(boolean nextShowLock) {
enableLock = false;
this.nextShowLock = nextShowLock;
}
}
LockActivity(只需要调用disablePatternLock(false)方法,再重写onBackPressed()即可):
public class LockActivity extends BaseActivity {
private static final String TAG = LockActivity.class.getSimpleName();
private Context mContext;
private MyApp myApp;
private TextView mTextView;
private GestureLockViewGroup mGesture;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_lock);
mContext = this;
myApp = MyApp.getInstance();
// 禁止唤起手势页
disablePatternLock(false);
initView();
}
private void initView() {
mTextView = (TextView) findViewById(R.id.tv_prompt_lock);
mTextView.setText("请绘制手势密码");
mGesture = (GestureLockViewGroup) findViewById(R.id.gesture_lock_view_group_lock);
mGesture.setAnswer(myApp.getSettings().getGesture());
mGesture.setShowPath(Setting.SHOW_PATH.equals(myApp.getSettings().getShowPath()));
mGesture.setOnGestureLockViewListener(mListener);
}
@Override
public void onBackPressed() {
// 阻止Lock页面的返回事件
moveTaskToBack(true);
}
/**
* 处理手势图案的输入结果
* @param matched
*/
private void gestureEvent(boolean matched) {
if (matched) {
mTextView.setText("输入正确");
finish();
} else {
mTextView.setText("手势错误,还剩"+ mGesture.getTryTimes() + "次");
}
}
/**
* 处理输错次数超限的情况
*/
private void unmatchedExceedBoundary() {
// 正常情况这里需要做处理(如退出或重登)
Toast.makeText(mContext, "错误次数太多,请重新登录", Toast.LENGTH_SHORT).show();
}
// 手势操作的回调监听
private GestureLockViewGroup.OnGestureLockViewListener mListener = new
GestureLockViewGroup.OnGestureLockViewListener() {
@Override
public void onBlockSelected(int cId) {
}
@Override
public void onGestureEvent(boolean matched) {
gestureEvent(matched);
}
@Override
public void onUnmatchedExceedBoundary() {
unmatchedExceedBoundary();
}
@Override
public void onFirstSetPattern(boolean patternOk) {
}
};
}
第一种方案的实现基本就这些东西了,更详细的代码后面会放上。
二、任何进入后台再切回的行为都会唤起手势:
要实现第二种方案,把时间改小一点,例如改成1s就可以大致实现这种效果,但如果一个页面的onCreate()耗时太长,超过1s,那就GG了。。。
总之这种方式不靠谱,所以我们不改时间,直接重写BaseActivity的onStop()方法:
@Override
protected void onStop() {
super.onStop();
myApp.setLockTime(0);
}
试试看,效果立竿见影。
三、结:
手势密码的应用就是这样了,只要做到从后台唤起时显示手势界面,APP内部界面切换不显示手势即可。
本文中Demo相对简单,还有些问题需要具体考虑:
1、非栈顶的Activity有可能会被回收,LockActivity页面也是调用moveToBack()切入后台,因此需要合理处理内存回收造成的变量重置问题;
2、设置变更会造成页面重置的问题,例如会调用MainActivity的 onDestroy() -> onCreate(),会唤起手势密码,需要合适的处理。
3、第二种方案仅仅是将 lockTime 时间变量当做一个标志,可以简化,onPause置“1”,onResume判断是否为"1",onStop置“0”。
--------------------------------------------------------------- 分割线 ----------------------------------------------------------
放上启动页和设置页代码:
SplashActivity(布局页面内容随意):
public class SplashActivity extends BaseActivity {
private static final String TAG = SplashActivity.class.getSimpleName();
private Context mContext;
private static int WAITING_TIME = 1000;// 启动页最大等待时间
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_splash);
mContext = this;
// 启动页不开启手势
disablePatternLock(true);
// 开启倒计时
timer.start();
// 读取设置
MyApp.getInstance().setSettings(loadSettings());
}
// 计时器总共一秒
private CountDownTimer timer = new CountDownTimer(WAITING_TIME, 300) {
@Override
public void onTick(long millisUntilFinished) {
}
@Override
public void onFinish() {
startActivity(new Intent(mContext, MainActivity.class));
finish();
}
};
/**
* 读取手势设置
* @return
*/
private Setting loadSettings() {
Setting setting = null;
try {
ObjectInputStream in = new ObjectInputStream(mContext.openFileInput("setting.txt"));
setting = (Setting) in.readObject();
in.close();
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return setting;
}
}
设置手势的页面:
activity_lock_on.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:zhy="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="gesture.test.liao.gesturelock.activity.LockOnActivity">
<TextView
android:id="@+id/tv_prompt_lock_on"
tools:text="hhh"
android:gravity="center"
android:layout_marginTop="@dimen/activity_horizontal_margin"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<gesture.test.liao.gesturelock.view.GestureLockViewGroup
android:id="@+id/gesture_lock_view_group_lock_on"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ffffff"
android:gravity="center_vertical"
zhy:count="3"
zhy:tryTimes="5"
zhy:color_no_finger_inner_circle="#00000000"
zhy:color_no_finger_outer_circle="#ff3595ff"
zhy:color_finger_on="#ff3595ff" />
</RelativeLayout>
LockOnActivity.class:
/**
* 设置手势密码
*/
public class LockOnActivity extends BaseActivity {
private static final String TAG = LockOnActivity.class.getSimpleName();
private TextView mTextView;
private GestureLockViewGroup mGesture;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_lock_on);
setTitle("设置手势密码");
initView();
}
private void initView() {
mTextView = (TextView) findViewById(R.id.tv_prompt_lock_on);
mTextView.setText("请绘制手势密码");
mGesture = (GestureLockViewGroup) findViewById(R.id.gesture_lock_view_group_lock_on);
mGesture.isFirstSet(true);
mGesture.setUnMatchExceedBoundary(10000);
mGesture.setOnGestureLockViewListener(mListener);
}
private void gestureEvent(boolean matched) {
if (matched) {
mTextView.setText("设置成功");
Setting setting = new Setting(mGesture.getChooseStr(),Setting.SHOW_PATH);
MyApp.getInstance().setSettings(setting);
setResult(RESULT_OK);
finish();
} else {
mTextView.setText("手势不一致,请重试");
}
}
private void firstSetPattern(boolean patternOk) {
if (patternOk) {
mTextView.setText("请再次输入以确认");
} else {
mTextView.setText("需要四个点以上");
}
}
// 回调监听
private GestureLockViewGroup.OnGestureLockViewListener mListener = new
GestureLockViewGroup.OnGestureLockViewListener() {
@Override
public void onBlockSelected(int cId) {
}
@Override
public void onGestureEvent(boolean matched) {
gestureEvent(matched);
}
@Override
public void onUnmatchedExceedBoundary() {
}
@Override
public void onFirstSetPattern(boolean patternOk) {
firstSetPattern(patternOk);
}
};
}
上面设置手势的界面由MainActivity调用startActivityForResult()打开,MainActivity:
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".activity.MainActivity">
<Button
android:id="@+id/btn_to_set_lock"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
MainActivity.class:
public class MainActivity extends BaseActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private static final int REQUEST_CODE_LOCK = 1;
private Context mContext;
private MyApp myApp;
private Switch swShowPath;
private Button btnToSub;
private Button btnToLock;
private RelativeLayout rlShowPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle("主页");
mContext = this;
myApp = MyApp.getInstance();
initView();
}
private void initView() {
btnToLock = (Button) findViewById(R.id.btn_to_set_lock);
btnToLock.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toSetLock();
}
});
}
private void toSetLock() {
Intent intent;
if (myApp.getSettings() == null || "".equals(myApp.getSettings().getGesture())) {
intent = new Intent(mContext, LockOnActivity.class);
startActivityForResult(intent, REQUEST_CODE_LOCK);
}
}
/**
* 存手势设置
*/
private void savePattern() {
try {
ObjectOutputStream out = new ObjectOutputStream(
mContext.openFileOutput("setting.txt", MODE_PRIVATE));
out.writeObject(myApp.getSettings());
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
savePattern();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 不置零的效果是,如果在设定的一分钟之内再次打开APP则不会弹出手势密码
// 因为应用的Activity全部finish后Application可能还存在
// 这句置零代码也可以放在启动APP页面onResume()方法之前(第二种方案无需置零)
myApp.setLockTime(0);
}
}
DEMO:http://download.csdn.net/download/u013258802/9685863
有什么问题欢迎回复探讨~