最近項目需求:要求在項目中添加手勢密碼和指紋驗證,恰巧最近在苦練自定義View卸勺,于是參考了網(wǎng)上輪子和自己的理解,實現(xiàn)了如下的效果。
國際慣例:Without pic you say a JB(獎杯).
這GIF做的是真的垃圾逻族,感興趣的去看Demo把,后面我會放上鏈接的骄崩。
一聘鳞、分析效果圖:
所有的自定義view都是通過分析效果圖薄辅,一點一點將效果圖分解成一個個模塊,然后單個模塊實現(xiàn)抠璃,最后拼裝成一個整體站楚,下面就通過手勢密碼的效果圖我們來剖析一波吧。
從上圖我們可以把View剖析如下:
(1)View的總大小我們可以通過手勢大View的寬度+手勢小View的高度搏嗡,通過onMeasure方法setMeasuredDimension(witdh窿春,width+minHeight)來賦予布局大小,minHeight可以根據(jù)實際情況自己賦值采盒。
(2)手勢大View可以通過onMeasure方法旧乞,通過比較寬高得到最小值,來設(shè)置手勢大View的正方形大小
(3)手勢小View同理于手勢大View磅氨,提示文字的位置尺栖,我們也很容易確認(rèn)。
(4)手勢大view的寬高得到了烦租,那么手勢大view每一個手勢點的坐標(biāo)和大小 我們就很容易得到延赌。
(5)相信大家初學(xué)Java的時候肯定做過,用 * 號打印各種圖形的操作叉橱,手勢view相當(dāng)于一個簡單的3*3矩陣挫以。我們知道了大小和坐標(biāo)很容易畫出來。
(6)小View也同理 可以實現(xiàn)窃祝,需要注意的是 在手勢密碼第一次注冊的時候存在小View掐松,在認(rèn)證的時候無小view,我們可以根據(jù)狀態(tài)锌杀,在onDraw中設(shè)置隱藏甩栈。
二、分析完后糕再,我們就一步一步來實現(xiàn)吧:
1量没、首先模板應(yīng)該具有通用性與可定制性,我們需要定義一個attrs突想。
通過效果圖分析殴蹄,我定義的attrs如下,在這里面猾担,手勢點我采用的是圖片(圖片可以讓手勢點更酷炫)
<declare-styleable name="SecurityCenter">
<!-- 選中狀態(tài)的手勢點-->
<attr name="selectedBitmap" format="reference"/>
<!-- 未選中狀態(tài)的手勢點-->
<attr name="unselectedBitmap" format="reference"/>
<!-- 選中狀態(tài)的手勢小點-->
<attr name="selectedBitmapSmall" format="reference"/>
<!-- 未選中狀態(tài)的手勢小點-->
<attr name="unselectedBitmapSmall" format="reference"/>
<!-- 驗證失敗后再次驗證的攔截時間-->
<attr name="waitTime" format="integer"/>
<!-- 驗證的最大失敗次數(shù)-->
<attr name="maxFailCounts" format="integer"/>
<!-- 繪制時最少連接的點數(shù)-->
<attr name="minPoint" format="integer"/>
<!-- 字體的顏色-->
<attr name="paintColor" format="color"/>
<!-- 字體的大小-->
<attr name="paintTextSize" format="dimension"/>
</declare-styleable>
2袭灯、在View中接收賦值,點數(shù)圖片我用的bitmap绑嘹,如果無具體定義稽荧,這些屬性都會給他默認(rèn)值。代碼如下:
public ChaosGestureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.SecurityCenter);
Drawable dw_selected = ta.getDrawable(R.styleable.SecurityCenter_selectedBitmap);
Drawable dw_unSeclect = ta.getDrawable(R.styleable.SecurityCenter_unselectedBitmap);
Drawable dw_selected_small = ta.getDrawable(R.styleable.SecurityCenter_selectedBitmapSmall);
Drawable dw_unSeclect_small = ta.getDrawable(R.styleable.SecurityCenter_unselectedBitmapSmall);
if (dw_selected!=null){
selectedBitmap = ((BitmapDrawable) dw_selected).getBitmap();
}else{
selectedBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_selected);
}
if (dw_unSeclect!=null){
unSelectedBitmap = ((BitmapDrawable) dw_unSeclect).getBitmap();
}else{
unSelectedBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_unselected);
}
if (dw_selected_small!=null){
selectedBitmapSmall = ((BitmapDrawable) dw_selected_small).getBitmap();
}else{
selectedBitmapSmall = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_selected_small);
}
if (dw_unSeclect_small!=null){
unSelectedBitmapSmall= ((BitmapDrawable) dw_unSeclect_small).getBitmap();
}else{
unSelectedBitmapSmall = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_finger_unselected_new);
}
//等待時間,默認(rèn)30s
waitTime = ta.getInteger(R.styleable.SecurityCenter_waitTime,30);
//嘗試次數(shù)工腋,默認(rèn)5
tempCount = ta.getInteger(R.styleable.SecurityCenter_maxFailCounts,5);
//最小設(shè)置的點,默認(rèn)4個
minPointNums = ta.getInteger(R.styleable.SecurityCenter_minPoint,4);
//設(shè)置畫筆的顏色
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setStrokeWidth(10);
mPaint.setStyle(Paint.Style.STROKE);
//畫筆的顏色
int color = ta.getColor(R.styleable.SecurityCenter_paintColor, context.getResources().getColor(R.color.black));
mPaint.setColor(color);
//字體的大小
float textsize = ta.getDimension(R.styleable.SecurityCenter_paintTextSize, 40);
mPaint.setTextSize(textsize);
//避免重新創(chuàng)建時候的錯誤
ta.recycle();
initView(context);
}
3姨丈、在onMeasure中繪測手勢View布局大小,通過最開始的分析畅卓,都很容易理解。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//width即為大View的單位寬 高
int width = Math.min(widthSize, heightSize);
if (widthMode == MeasureSpec.UNSPECIFIED) {
width = heightSize;
} else if (heightMode == MeasureSpec.UNSPECIFIED) {
width = widthSize;
}
//大View一行3*1單位行高
mLineHeight = width / 3;
//大手勢View為邊長width的正方形,panelHeight是給小手勢view預(yù)留的空間
setMeasuredDimension(width, width + panelHeight);
}
4蟋恬、通過onSizeChange方法可以獲取翁潘,根據(jù)mLineHeight(3*1的行高) 的值,可以定義大手勢密碼點和小手勢密碼點的寬高歼争,然后通過Bitmap.createScaledBitmap方法拜马,設(shè)置好手勢點的大小圖。細(xì)節(jié)可以看代碼注解如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mPanelWidth = Math.min(w, h);
//大手勢點寬度沐绒,為單位寬高的0.6倍俩莽,顯得更好看一些不會很滿
pieceWidth = (int) (mLineHeight * 0.6f);
//小手勢點寬度,同理
pieceWidthSmall = (int) (mLineHeight * 0.15f);
//畫出對應(yīng)手勢點的大小
selectedBitmap = Bitmap.createScaledBitmap(selectedBitmap, (int) pieceWidth, (int) pieceWidth, false);
unSelectedBitmap = Bitmap.createScaledBitmap(unSelectedBitmap, (int) pieceWidth, (int) pieceWidth, false);
selectedBitmapSmall = Bitmap.createScaledBitmap(selectedBitmapSmall, (int) pieceWidthSmall, (int) pieceWidthSmall, false);
unSelectedBitmapSmall = Bitmap.createScaledBitmap(unSelectedBitmapSmall, (int) pieceWidthSmall, (int) pieceWidthSmall, false);
}
5洒沦、我們知道GestureView一般分為兩種狀態(tài)豹绪,一種是注冊狀態(tài)(包含小view的那種),另一種是認(rèn)證狀態(tài)(不包含小view的那種)申眼,所以我們要定義兩種狀態(tài),來區(qū)分使用情況蝉衣。
//手勢初始化錄入狀態(tài)
public static final int STATE_REGISTER = 101;
//手勢確認(rèn) 使用狀態(tài)
public static final int STATE_LOGIN = 100;
//設(shè)置一個參數(shù)記錄當(dāng)前是出于初始化階段還是使用階段括尸,默認(rèn)為確認(rèn)狀態(tài)
private int stateFlag = STATE_LOGIN;
那么我們在每次注冊成功的時候,需要保存手勢狀態(tài)病毡,我們給狀態(tài)存在SharedPreferences中
//成功后保存狀態(tài)
private boolean saveState() {
SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
SharedPreferences.Editor edit = sp.edit();
edit.putInt("state", stateFlag);
return edit.commit();
}
同理在初始化之前濒翻,我們要得到狀態(tài),判斷當(dāng)前view屬于什么狀態(tài)啦膜,這樣才能判斷onDraw中是否繪制小View
//從SP中獲取當(dāng)前View處于什么狀態(tài)有送,默認(rèn)為初始化狀態(tài)
private int getState() {
SharedPreferences mSharedPreference = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
return mSharedPreference.getInt("state", STATE_REGISTER);
}
6、根據(jù)狀態(tài)繪制手勢密碼點僧家,連接線雀摘,和提示文字
(1)繪制9個未選中狀態(tài)的大手勢View點,通過canvas.drawBitmap(Bitmap bitmap, float left, float top, Paint paint)方法繪制八拱,這里我們要注意位置計算的時候阵赠,只需要注意在android屏幕坐標(biāo)系里,左上角的位置是(0肌稻,0)清蚀,往右往下為正。
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
canvas.drawBitmap(unSelectedBitmap, (float) (mLineHeight * (j + 0.5) - pieceWidth / 2), (float) (mLineHeight * (i + 0.5) - pieceWidth / 2 + panelHeight), mPaint);
}
}
可能這么說有點抽象爹谭,不過也就是把坐標(biāo)搞清楚了還是很簡單的枷邪,畫張圖配合你理解,圖中小View預(yù)留高度(panelHeight):
(2)可能到現(xiàn)在你就會很好奇诺凡,我設(shè)置的手勢連線到底是怎么存儲和校驗的呢东揣?問的好药薯! 這個問題我開始也思考了很久,有輪子是通過一個二維數(shù)組實現(xiàn)的救斑,通過這個二維數(shù)組我來了思路童本,聯(lián)想到了Bean。我用Bean存儲對應(yīng)點的X和Y的坐標(biāo)脸候,把每個點的實例加入一個List<>中穷娱,就完成了手勢繪制所有點的存儲。
bean的代碼如下:
//定義Bean运沦,來存儲手勢坐標(biāo)
public class GestureBean {
private int x;
private int y;
@Override
public String toString() {
return "GestureBean{" +
"x=" + x +
", y=" + y +
'}';
}
public GestureBean(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
@Override
public boolean equals(Object o) {
return ((GestureBean) o).getX() == x && ((GestureBean) o).getY() == y;
}
}
(3)繪制連接線和選中點:連接線是通過過 onTouchEvent和onDraw泵额,配合畫出的,在OnTouchEvent中手指經(jīng)過的點都會存在listDatas集合中携添,再通過 invalidate();方法通知onDraw嫁盲,根據(jù)listDatas中的新增點數(shù),來繪制出選中點和點之間的連接線烈掠。再此只給出onDraw中的代碼羞秤,onTouchEvent中的邏輯會在下文詳細(xì)說明。
//用于判斷狀態(tài)
GestureBean firstGestrue = null;
GestureBean currGestrue = null;
if (!listDatas.isEmpty()) {
firstGestrue = listDatas.get(0);
//畫連接線
for (int i = 1; i < listDatas.size(); i++) {
currGestrue = listDatas.get(i);
canvas.drawLine((float) (mLineHeight * (firstGestrue.getX() + 0.5)), (float) (mLineHeight * (firstGestrue.getY() + 0.5) + panelHeight), (float) (mLineHeight * (currGestrue.getX() + 0.5)), (float) (mLineHeight * (currGestrue.getY() + 0.5) + panelHeight), mPaint);
firstGestrue = currGestrue;
}
//最后一條線
lastGestrue = listDatas.get(listDatas.size() - 1);
canvas.drawLine((float) (mLineHeight * (lastGestrue.getX() + 0.5)), (float) (mLineHeight * (lastGestrue.getY() + 0.5) + panelHeight), currX, currY, mPaint);
//遍歷數(shù)組左敌,把把選中的點更換圖片
for (GestureBean bean : listDatas) {
canvas.drawBitmap(selectedBitmap, (float) (mLineHeight * (bean.getX() + 0.5) - pieceWidth / 2), (float) (mLineHeight * (bean.getY() + 0.5) + panelHeight - pieceWidth / 2), mPaint);
}
}
注冊手勢成功的時候需要將手勢集合瘾蛋,保存,用于下一次校驗矫限,我們存在SharedPreference中(注意:這個手勢View只適用于本機攔截哺哼,所以存SharedPresference就夠了)
//將點的xy list存入sp
private boolean saveToSharedPrefference(List<GestureBean> data) {
SharedPreferences sp = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
SharedPreferences.Editor edit = sp.edit();
//存入多少個點
edit.putInt("data_size", data.size()); /*sKey is an array*/
//和每個店的坐標(biāo)
for (int i = 0; i < data.size(); i++) {
edit.remove("data_" + i);
edit.putString("data_" + i, data.get(i).getX() + " " + data.get(i).getY());
}
return edit.commit();
}
獲取存儲集合:,我們再存的時候和取得時候,可以先存一個錄入點的數(shù)量叼风,更方便做判斷取董。
//讀取之前保存的List
public List<GestureBean> loadSharedPrefferenceData() {
List<GestureBean> list = new ArrayList<>();
SharedPreferences mSharedPreference = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
//取出點數(shù)
int size = mSharedPreference.getInt("data_size", 0);
//和坐標(biāo)
for (int i = 0; i < size; i++) {
String str = mSharedPreference.getString("data_" + i, "0 0");
list.add(new GestureBean(Integer.parseInt(str.split(" ")[0]), Integer.parseInt(str.split(" ")[1])));
}
return list;
}
(4) 小圖和文字的繪制就很簡單了,參考前幾項 我就直接給代碼了(剛才開會无宿,思路被干擾了茵汰。。懈贺。经窖。)
//如果處于初始化狀態(tài)
if (stateFlag == STATE_REGISTER) {
//繪制上面的提示點 不需要提示點
drawTipsPoint(canvas);
} else {
//上面的是文字 點沒了
drawTipsText(canvas);
}
需要注意的是,小View在完成第一次繪制的時候梭灿,第二次繪制的時候需要保存第一次的樣式画侣,通過list存儲比較,如下代碼實現(xiàn)堡妒。
//繪制提示點
private void drawTipsPoint(Canvas canvas) {
//寬度為View寬度的一半
float widthMiddleX = mPanelWidth / 2;
//確定好相關(guān)坐標(biāo)配乱,找出第一個點的中心點
float firstX = widthMiddleX - pieceWidthSmall / 4 - pieceWidthSmall / 2 - pieceWidthSmall;
float firstY = panelHeight / 2 - pieceWidthSmall / 2 - pieceWidthSmall - pieceWidthSmall / 4 - 10;
//畫點,由于沒有選中,畫9個未選中點
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
canvas.drawBitmap(unSelectedBitmapSmall, (float) (firstX + j * (pieceWidthSmall * 1.25)), (float) (firstY + i * (pieceWidthSmall * 1.25)), mPaint);
}
}
//第二次確認(rèn)前的小手勢密碼·顯示第一次劃過的痕跡
if (listDatasCopy != null && !listDatasCopy.isEmpty()) {
for (GestureBean bean : listDatasCopy) {
canvas.drawBitmap(selectedBitmapSmall, (float) (firstX + bean.getX() * (pieceWidthSmall * 1.25)), (float) (firstY + bean.getY() * (pieceWidthSmall * 1.25)), mPaint);
}
}
//隨著手指ActionMove來改變選中點的顏色
else if (listDatas != null && !listDatas.isEmpty()) {
for (GestureBean bean : listDatas) {
canvas.drawBitmap(selectedBitmapSmall, (float) (firstX + bean.getX() * (pieceWidthSmall * 1.25)), (float) (firstY + bean.getY() * (pieceWidthSmall * 1.25)), mPaint);
}
}
drawMessage(canvas, "繪制解鎖圖案", mError);
}
效果圖如下:
繪制文字搬泥,確定好大體坐標(biāo)就可了桑寨,在小view下面,很好理解忿檩,直接給代碼了:
//繪制提示語
private void drawTipsText(Canvas canvas) {
float widthMiddleX = mPanelWidth / 2;
mPaint.setStyle(Paint.Style.FILL);
int widthStr1 = (int) mPaint.measureText("輸入手勢來解鎖");
float baseX = widthMiddleX - widthStr1 / 2;
float baseY = panelHeight / 2 + 50;
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float fontTotalHeight = fontMetrics.bottom - fontMetrics.top;
float offY = fontTotalHeight / 2 - fontMetrics.bottom - 30;
float newY = baseY + offY;
canvas.drawText("輸入手勢來解鎖", baseX, newY, mPaint);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setStrokeWidth(10);
}
7尉尾、手勢密碼設(shè)置,必然要處理OnTouchEvent燥透,這里的邏輯才是關(guān)鍵沙咏,我會詳細(xì)分析。
(1)在這里我們封裝的比較完善班套,我處理了驗證超過驗證次數(shù)會攔截手勢View肢藐,這里面算是后期完善,但是為大家梳理思路的話就顯得比較冗余吱韭,直接貼代碼吆豹,先pass掉,敢興趣去看demo:
if (mTimeOut) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (0 < leftTime && leftTime <= 30) {
AlertUtil.t(mContext, "嘗試次數(shù)達(dá)到最大," + leftTime + "s后重試");
}
return true;
}
}
(2)首先我們要判斷我們的OnTouch事件理盆,是否在大View的范圍內(nèi)痘煤,由于坐標(biāo)開始規(guī)范的很清楚,這個很好判斷
if (event.getY() >= ((mLineHeight * (0 + 0.5) - pieceWidth / 2 + panelHeight))){
//得到XY用于判斷 手指處于哪個點
int x = (int) ((event.getY() - panelHeight) / mLineHeight);
int y = (int) (event.getX() / mLineHeight);
//當(dāng)前手指的坐標(biāo)
currX = event.getX();
currY = event.getY();
}
(3)MotionEvent.ACTION_DOWN: 當(dāng)手指按下去的時候熏挎,我們要判斷按下去的點速勇,處于哪一個大手勢點范圍內(nèi),并把它加入List<Bean>中坎拐。通知onDraw重繪,如上文所說的那樣养匈,把點改為選中狀態(tài)哼勇。
case MotionEvent.ACTION_DOWN:
lastGestrue = null;
if (currX >= 0 && currX <= mPanelWidth && currY >= panelHeight && currY <= panelHeight + mPanelWidth) {
if (currY <= (x + 0.5) * mLineHeight + pieceWidth / 2 + panelHeight && currY >= (x + 0.5) * mLineHeight - pieceWidth / 2 + panelHeight &&
currX <= (y + 0.5) * mLineHeight + pieceWidth / 2 && currX >= (y + 0.5) * mLineHeight - pieceWidth / 2) {
//判斷當(dāng)前手指處于哪個點范圍內(nèi),如果點沒存在listData,存進(jìn)去呕乎,第一個點
if (!listDatas.contains(new GestureBean(y, x))) {
listDatas.add(new GestureBean(y, x));
}
}
}
//重繪一次积担,第一個點顯示被選中了
invalidate();
break;
(4)MotionEvent.ACTION_MOVE: 手指在View上滑動,滑動到哪個點就把猬仁,哪個點的坐標(biāo)add到List<Baen>中帝璧。在通知重繪
case MotionEvent.ACTION_MOVE:
//手指移動在大View范圍內(nèi)
if (currX >= 0 && currX <= mPanelWidth && currY >= panelHeight && currY <= panelHeight + mPanelWidth) {
//縮小響應(yīng)范圍 在此處需要注意的是 x跟currX在物理方向上是反的哦
if (currY <= (x + 0.5) * mLineHeight + pieceWidth / 2 + panelHeight && currY >= (x + 0.5) * mLineHeight - pieceWidth / 2 + panelHeight &&
currX <= (y + 0.5) * mLineHeight + pieceWidth / 2 && currX >= (y + 0.5) * mLineHeight - pieceWidth / 2) {
//滑倒的店處于哪個點范圍內(nèi),如果點沒存在listData,存進(jìn)去
if (!listDatas.contains(new GestureBean(y, x))) {
listDatas.add(new GestureBean(y, x));
//
}
}
}
//重繪
invalidate();
break;
(5)MotionEvent.ACTION_UP: 分為兩種情況湿刽,
1的烁、認(rèn)證狀態(tài),會從loadSharedPrefferenceData獲取到以前的listdatas做比較诈闺,判斷是否成功
2渴庆、注冊狀態(tài),會比較第一次的listdatas,來判斷兩次驗證是否一致,從而襟雷,處理成功和失敗的邏輯刃滓。
case MotionEvent.ACTION_UP:
if (lastGestrue != null) {
currX = (float) ((lastGestrue.getX() + 0.5) * mLineHeight);
currY = (float) ((lastGestrue.getY() + 0.5) * mLineHeight);
}
//如果View處于認(rèn)證狀態(tài)
if (stateFlag == STATE_LOGIN) {
//相同那么認(rèn)證成功
if (listDatas.equals(loadSharedPrefferenceData())) {
mError = false;
postListener(true);
invalidate();
listDatas.clear();
return true;
} else {
if (--tempCount == 0) {//嘗試次數(shù)達(dá)到上限
mError = true;
mTimeOut = true;
listDatas.clear();
Date date = new Date();
PreferenceCache.putGestureTime(date.getTime());
mTimerTask = new InnerTimerTask(handler);
mTimer.schedule(mTimerTask, 0, 1000);
invalidate();
return true;
}
mError = true;
AlertUtil.t(mContext, "手勢錯誤,還可以再輸入" + tempCount + "次");
listDatas.clear();
}
}
//View處于注冊狀態(tài)
else if (stateFlag == STATE_REGISTER) {
//第一次認(rèn)證狀態(tài)
if (listDatasCopy == null || listDatasCopy.isEmpty()) {
if (listDatas.size() < minPointNums) {
listDatas.clear();
mError = true;
AlertUtil.t(mContext, "點數(shù)不能小于" + minPointNums + "個");
invalidate();
return true;
}
listDatasCopy.addAll(listDatas);
listDatas.clear();
mError = false;
AlertUtil.t(mContext, "請再一次繪制");
} else {
//兩次認(rèn)證成功
if (listDatas.equals(listDatasCopy)) {
saveToSharedPrefference(listDatas);
mError = false;
stateFlag = STATE_LOGIN;
postListener(true);
saveState();
} else {
mError = true;
AlertUtil.t(mContext, "與上次手勢繪制不一致,請重新設(shè)置");
}
listDatas.clear();
invalidate();
return true;
}
}
invalidate();
break;
至此,手勢View的所有邏輯大概已經(jīng)清楚了耸弄,下面做的是需要完善咧虎。
三·、完善View计呈,設(shè)置接口調(diào)用砰诵,失敗倒計時,和關(guān)閉View是清理當(dāng)前SP緩存震叮。
1胧砰、仔細(xì)看的朋友會發(fā)現(xiàn),ACTION.UP中有 postListener(true)這些東西苇瓣,這就是我定義的接口尉间,來返回認(rèn)證狀態(tài)。
(1)定義接口,三個參分別為:view所處狀態(tài)击罪,存儲的List哲嘲,是否成功(注冊或認(rèn)證)
//定義接口 ,傳遞View狀態(tài)
public interface GestureCallBack{
void gestureVerifySuccessListener(int stateFlag, List<GestureBean> data, boolean success);
}
(2)實例化接口媳禁,當(dāng)前Activity必須繼承接口
//讓當(dāng)前的Activity繼承View的接口
try {
gestureCallBack = (GestureCallBack) mContext;
} catch (final ClassCastException e) {
throw new ClassCastException(mContext.toString() + " must implement GestureCallBack");
}
(3)給接口傳遞眠副,實時數(shù)據(jù)Action.UP
//給接口傳遞數(shù)據(jù)
private void postListener(boolean success) {
if (gestureCallBack != null) {
gestureCallBack.gestureVerifySuccessListener(stateFlag, listDatas, success);
}
}
2、驗證失敗倒計時竣稽,通過Handler囱怕,Timer和TimerTask實現(xiàn)
(1)、內(nèi)部類TimeTask
//定義一個內(nèi)部TimerTask類用于記錄毫别,錯誤倒計時
static class InnerTimerTask extends TimerTask{
Handler handler;
public InnerTimerTask(Handler handler) {
this.handler = handler;
}
@Override
public void run() {
handler.sendMessage(handler.obtainMessage());
}
}
(2)實例化娃弓,上次錯誤時間也是存儲在SharedPreference,waiTime為定義好的超時時間岛宦。代碼如下
mTimer = new Timer();
//計算上次失敗時間與現(xiàn)在的時間差
try {
long lastTime = PreferenceCache.getGestureTime();
Date date = new Date();
if (lastTime !=0 && (date.getTime()-lastTime)/1000<waitTime){
//失敗時間未到台丛,還處于鎖定狀態(tài)
mTimeOut = true;
leftTime = (int)(waitTime-((date.getTime()-lastTime))/1000);
mTimerTask = new InnerTimerTask(handler);
mTimer.schedule(mTimerTask,0,1000);
}else{
mTimeOut = false;
leftTime = waitTime;
}
}catch (RuntimeException e){
e.printStackTrace();
}
(3)Handler處理消息:
//接受TimerTask消息,通知UI
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
leftTime--;
if (leftTime == 0){
if (mTimer != null)
mTimerTask.cancel();
mTimeOut = false;
AlertUtil.t(mContext,"請繪制解鎖圖案");
mError = false;
invalidate();
//將計時信息還原
reSet();
return;
}
mError = true;
invalidate();
}
};
3、清理手勢View緩存砾肺,用于關(guān)閉View或者修改密碼
//清除以前保存的狀態(tài)挽霉,用于關(guān)閉View
public boolean clearCache() {
SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
SharedPreferences.Editor edit = sp.edit();
edit.putInt("state", STATE_REGISTER);
stateFlag = STATE_REGISTER;
invalidate();
return edit.commit();
}
//用于更改手勢密碼,清除以前密碼
public boolean clearCacheLogin() {
SharedPreferences sp = mContext.getSharedPreferences("STATE_DATA", Activity.MODE_PRIVATE);
SharedPreferences.Editor edit = sp.edit();
edit.putInt("state", STATE_LOGIN);
stateFlag = STATE_LOGIN;
invalidate();
return edit.commit();
}
四变汪、簡單使用:
1侠坎、以設(shè)置手勢密碼 為例:
(1)XML布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.chaos.chaossecuritycenter.activity.SettingPatternPswActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp">
<TextView
android:id="@+id/tv_setting_back"
android:layout_marginLeft="10dp"
android:textSize="16sp"
android:drawableLeft="@mipmap/back"
android:textColor="@color/bak_blue"
android:gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="返回"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="設(shè)置手勢密碼"
android:textColor="@color/black"
android:textSize="20sp" />
</RelativeLayout>
<com.chaos.chaossecuritycenter.weight.ChaosGestureView
android:id="@+id/gesture"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginLeft="40dp"
android:layout_marginRight="40dp"
android:layout_marginTop="40dp"
android:layout_weight="1"
app:selectedBitmap="@mipmap/icon_finger_selected"
app:unselectedBitmap="@mipmap/icon_finger_unselected"
app:selectedBitmapSmall="@mipmap/icon_finger_selected_small"
app:unselectedBitmapSmall="@mipmap/icon_finger_unselected_new"
app:waitTime="30"
app:maxFailCounts="5"
app:minPoint="4"
app:paintColor="@color/bak_blue"
app:paintTextSize="15sp"
/>
</LinearLayout>
布局預(yù)覽:
Java代碼(設(shè)置手勢密碼頁面):
public class SettingPatternPswActivity extends AppCompatActivity implements ChaosGestureView.GestureCallBack{
private TextView tv_back;
private ChaosGestureView gestureView;
private int jumpFlg;
private int flag;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting_pattern_psw);
jumpFlg = getIntent().getIntExtra("jumpFlg", 0);
flag = getIntent().getIntExtra("flag", 0);
initView();
}
private void initView() {
tv_back = (TextView) findViewById(R.id.tv_setting_back);
gestureView = (ChaosGestureView) findViewById(R.id.gesture);
gestureView.setGestureCallBack(this);
//不調(diào)用這個方法會造成第二次啟動程序直接進(jìn)入手勢識別而不是手勢設(shè)置
gestureView.clearCache();
tv_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
}
@Override
public void gestureVerifySuccessListener(int stateFlag, List<ChaosGestureView.GestureBean> data, boolean success) {
if (stateFlag == GestureView.STATE_LOGIN) {
PreferenceCache.putGestureFlag(true);
finish();
}
}
@Override
public void onPointerCaptureChanged(boolean hasCapture) {
}
}
五:總結(jié)
這個自定義手勢密碼,是我參照個別輪子+我本人的理解疫衩,仿照支付寶手勢密碼設(shè)計的硅蹦,整體流暢我已帶大家分析了一波荣德。該View還有很多需要完善的地方,我以后會慢慢完善童芹,有什么指教或者疑問涮瞻,請大家在下面留言。
Demo:
1假褪、手勢密碼自定義View
2署咽、指紋驗證(由于仿支付寶安全的Demo,含指紋我就一塊做了生音,用的三方)
Demo地址:https://github.com/ChaosOctopus/ChaosSecurityCenter
如果覺得對您有用宁否,請給我一個贊,或者一個Star缀遍。