天天看點

Android應用 手勢密碼的實作(四)

本文基于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

有什麼問題歡迎回複探讨~