本文基于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
有什麼問題歡迎回複探讨~