最近項(xiàng)目需求:要求在項(xiàng)目中添加手勢(shì)密碼和指紋驗(yàn)證,恰巧最近在苦練自定義View,于是參考了網(wǎng)上輪子和自己的理解,實(shí)現(xiàn)了如下的效果。
國(guó)際慣例:Without pic you say a JB(獎(jiǎng)杯).
這GIF做的是真的垃圾,感興趣的去看Demo把,后面我會(huì)放上鏈接的。
一、分析效果圖:
所有的自定義view都是通過(guò)分析效果圖,一點(diǎn)一點(diǎn)將效果圖分解成一個(gè)個(gè)模塊,然后單個(gè)模塊實(shí)現(xiàn),最后拼裝成一個(gè)整體,下面就通過(guò)手勢(shì)密碼的效果圖我們來(lái)剖析一波吧。
從上圖我們可以把View剖析如下:
(1)View的總大小我們可以通過(guò)手勢(shì)大View的寬度+手勢(shì)小View的高度,通過(guò)onMeasure方法setMeasuredDimension(witdh,width+minHeight)來(lái)賦予布局大小,minHeight可以根據(jù)實(shí)際情況自己賦值。
(2)手勢(shì)大View可以通過(guò)onMeasure方法,通過(guò)比較寬高得到最小值,來(lái)設(shè)置手勢(shì)大View的正方形大小
(3)手勢(shì)小View同理于手勢(shì)大View,提示文字的位置,我們也很容易確認(rèn)。
(4)手勢(shì)大view的寬高得到了,那么手勢(shì)大view每一個(gè)手勢(shì)點(diǎn)的坐標(biāo)和大小 我們就很容易得到。
(5)相信大家初學(xué)Java的時(shí)候肯定做過(guò),用 * 號(hào)打印各種圖形的操作,手勢(shì)view相當(dāng)于一個(gè)簡(jiǎn)單的3*3矩陣。我們知道了大小和坐標(biāo)很容易畫(huà)出來(lái)。
(6)小View也同理 可以實(shí)現(xiàn),需要注意的是 在手勢(shì)密碼第一次注冊(cè)的時(shí)候存在小View,在認(rèn)證的時(shí)候無(wú)小view,我們可以根據(jù)狀態(tài),在onDraw中設(shè)置隱藏。
二、分析完后,我們就一步一步來(lái)實(shí)現(xiàn)吧:
1、首先模板應(yīng)該具有通用性與可定制性,我們需要定義一個(gè)attrs。
通過(guò)效果圖分析,我定義的attrs如下,在這里面,手勢(shì)點(diǎn)我采用的是圖片(圖片可以讓手勢(shì)點(diǎn)更酷炫)
<declare-styleable name="SecurityCenter">
<!-- 選中狀態(tài)的手勢(shì)點(diǎn)-->
<attr name="selectedBitmap" format="reference"/>
<!-- 未選中狀態(tài)的手勢(shì)點(diǎn)-->
<attr name="unselectedBitmap" format="reference"/>
<!-- 選中狀態(tài)的手勢(shì)小點(diǎn)-->
<attr name="selectedBitmapSmall" format="reference"/>
<!-- 未選中狀態(tài)的手勢(shì)小點(diǎn)-->
<attr name="unselectedBitmapSmall" format="reference"/>
<!-- 驗(yàn)證失敗后再次驗(yàn)證的攔截時(shí)間-->
<attr name="waitTime" format="integer"/>
<!-- 驗(yàn)證的最大失敗次數(shù)-->
<attr name="maxFailCounts" format="integer"/>
<!-- 繪制時(shí)最少連接的點(diǎn)數(shù)-->
<attr name="minPoint" format="integer"/>
<!-- 字體的顏色-->
<attr name="paintColor" format="color"/>
<!-- 字體的大小-->
<attr name="paintTextSize" format="dimension"/>
</declare-styleable>
2、在View中接收賦值,點(diǎn)數(shù)圖片我用的bitmap,如果無(wú)具體定義,這些屬性都會(huì)給他默認(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);
}
//等待時(shí)間,默認(rèn)30s
waitTime = ta.getInteger(R.styleable.SecurityCenter_waitTime,30);
//嘗試次數(shù),默認(rèn)5
tempCount = ta.getInteger(R.styleable.SecurityCenter_maxFailCounts,5);
//最小設(shè)置的點(diǎn),默認(rèn)4個(gè)
minPointNums = ta.getInteger(R.styleable.SecurityCenter_minPoint,4);
//設(shè)置畫(huà)筆的顏色
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setStrokeWidth(10);
mPaint.setStyle(Paint.Style.STROKE);
//畫(huà)筆的顏色
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)建時(shí)候的錯(cuò)誤
ta.recycle();
initView(context);
}
3、在onMeasure中繪測(cè)手勢(shì)View布局大小,通過(guò)最開(kāi)始的分析,都很容易理解。
@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;
//大手勢(shì)View為邊長(zhǎng)width的正方形,panelHeight是給小手勢(shì)view預(yù)留的空間
setMeasuredDimension(width, width + panelHeight);
}
4、通過(guò)onSizeChange方法可以獲取,根據(jù)mLineHeight(3*1的行高) 的值,可以定義大手勢(shì)密碼點(diǎn)和小手勢(shì)密碼點(diǎn)的寬高,然后通過(guò)Bitmap.createScaledBitmap方法,設(shè)置好手勢(shì)點(diǎn)的大小圖。細(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);
//大手勢(shì)點(diǎn)寬度,為單位寬高的0.6倍,顯得更好看一些不會(huì)很滿(mǎn)
pieceWidth = (int) (mLineHeight * 0.6f);
//小手勢(shì)點(diǎn)寬度,同理
pieceWidthSmall = (int) (mLineHeight * 0.15f);
//畫(huà)出對(duì)應(yīng)手勢(shì)點(diǎn)的大小
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),一種是注冊(cè)狀態(tài)(包含小view的那種),另一種是認(rèn)證狀態(tài)(不包含小view的那種),所以我們要定義兩種狀態(tài),來(lái)區(qū)分使用情況。
//手勢(shì)初始化錄入狀態(tài)
public static final int STATE_REGISTER = 101;
//手勢(shì)確認(rèn) 使用狀態(tài)
public static final int STATE_LOGIN = 100;
//設(shè)置一個(gè)參數(shù)記錄當(dāng)前是出于初始化階段還是使用階段,默認(rèn)為確認(rèn)狀態(tài)
private int stateFlag = STATE_LOGIN;
那么我們?cè)诿看巫?cè)成功的時(shí)候,需要保存手勢(shì)狀態(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)繪制手勢(shì)密碼點(diǎn),連接線(xiàn),和提示文字
(1)繪制9個(gè)未選中狀態(tài)的大手勢(shì)View點(diǎn),通過(guò)canvas.drawBitmap(Bitmap bitmap, float left, float top, Paint paint)方法繪制,這里我們要注意位置計(jì)算的時(shí)候,只需要注意在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);
}
}
可能這么說(shuō)有點(diǎn)抽象,不過(guò)也就是把坐標(biāo)搞清楚了還是很簡(jiǎn)單的,畫(huà)張圖配合你理解,圖中小View預(yù)留高度(panelHeight):
(2)可能到現(xiàn)在你就會(huì)很好奇,我設(shè)置的手勢(shì)連線(xiàn)到底是怎么存儲(chǔ)和校驗(yàn)的呢?問(wèn)的好! 這個(gè)問(wèn)題我開(kāi)始也思考了很久,有輪子是通過(guò)一個(gè)二維數(shù)組實(shí)現(xiàn)的,通過(guò)這個(gè)二維數(shù)組我來(lái)了思路,聯(lián)想到了Bean。我用Bean存儲(chǔ)對(duì)應(yīng)點(diǎn)的X和Y的坐標(biāo),把每個(gè)點(diǎn)的實(shí)例加入一個(gè)List<>中,就完成了手勢(shì)繪制所有點(diǎn)的存儲(chǔ)。
bean的代碼如下:
//定義Bean,來(lái)存儲(chǔ)手勢(shì)坐標(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)繪制連接線(xiàn)和選中點(diǎn):連接線(xiàn)是通過(guò)過(guò) onTouchEvent和onDraw,配合畫(huà)出的,在OnTouchEvent中手指經(jīng)過(guò)的點(diǎn)都會(huì)存在listDatas集合中,再通過(guò) invalidate();方法通知onDraw,根據(jù)listDatas中的新增點(diǎn)數(shù),來(lái)繪制出選中點(diǎn)和點(diǎn)之間的連接線(xiàn)。再此只給出onDraw中的代碼,onTouchEvent中的邏輯會(huì)在下文詳細(xì)說(shuō)明。
//用于判斷狀態(tài)
GestureBean firstGestrue = null;
GestureBean currGestrue = null;
if (!listDatas.isEmpty()) {
firstGestrue = listDatas.get(0);
//畫(huà)連接線(xiàn)
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;
}
//最后一條線(xiàn)
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ù)組,把把選中的點(diǎn)更換圖片
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);
}
}
注冊(cè)手勢(shì)成功的時(shí)候需要將手勢(shì)集合,保存,用于下一次校驗(yàn),我們存在SharedPreference中(注意:這個(gè)手勢(shì)View只適用于本機(jī)攔截,所以存SharedPresference就夠了)
//將點(diǎn)的xy list存入sp
private boolean saveToSharedPrefference(List<GestureBean> data) {
SharedPreferences sp = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
SharedPreferences.Editor edit = sp.edit();
//存入多少個(gè)點(diǎn)
edit.putInt("data_size", data.size()); /*sKey is an array*/
//和每個(gè)店的坐標(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();
}
獲取存儲(chǔ)集合:,我們?cè)俅娴臅r(shí)候和取得時(shí)候,可以先存一個(gè)錄入點(diǎn)的數(shù)量,更方便做判斷。
//讀取之前保存的List
public List<GestureBean> loadSharedPrefferenceData() {
List<GestureBean> list = new ArrayList<>();
SharedPreferences mSharedPreference = mContext.getSharedPreferences("GESTURAE_DATA", Activity.MODE_PRIVATE);
//取出點(diǎn)數(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) 小圖和文字的繪制就很簡(jiǎn)單了,參考前幾項(xiàng) 我就直接給代碼了(剛才開(kāi)會(huì),思路被干擾了。。。。)
//如果處于初始化狀態(tài)
if (stateFlag == STATE_REGISTER) {
//繪制上面的提示點(diǎn) 不需要提示點(diǎn)
drawTipsPoint(canvas);
} else {
//上面的是文字 點(diǎn)沒(méi)了
drawTipsText(canvas);
}
需要注意的是,小View在完成第一次繪制的時(shí)候,第二次繪制的時(shí)候需要保存第一次的樣式,通過(guò)list存儲(chǔ)比較,如下代碼實(shí)現(xiàn)。
//繪制提示點(diǎn)
private void drawTipsPoint(Canvas canvas) {
//寬度為View寬度的一半
float widthMiddleX = mPanelWidth / 2;
//確定好相關(guān)坐標(biāo),找出第一個(gè)點(diǎn)的中心點(diǎn)
float firstX = widthMiddleX - pieceWidthSmall / 4 - pieceWidthSmall / 2 - pieceWidthSmall;
float firstY = panelHeight / 2 - pieceWidthSmall / 2 - pieceWidthSmall - pieceWidthSmall / 4 - 10;
//畫(huà)點(diǎn),由于沒(méi)有選中,畫(huà)9個(gè)未選中點(diǎn)
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)前的小手勢(shì)密碼·顯示第一次劃過(guò)的痕跡
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來(lái)改變選中點(diǎn)的顏色
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下面,很好理解,直接給代碼了:
//繪制提示語(yǔ)
private void drawTipsText(Canvas canvas) {
float widthMiddleX = mPanelWidth / 2;
mPaint.setStyle(Paint.Style.FILL);
int widthStr1 = (int) mPaint.measureText("輸入手勢(shì)來(lái)解鎖");
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("輸入手勢(shì)來(lái)解鎖", baseX, newY, mPaint);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setStrokeWidth(10);
}
7、手勢(shì)密碼設(shè)置,必然要處理OnTouchEvent,這里的邏輯才是關(guān)鍵,我會(huì)詳細(xì)分析。
(1)在這里我們封裝的比較完善,我處理了驗(yàn)證超過(guò)驗(yàn)證次數(shù)會(huì)攔截手勢(shì)View,這里面算是后期完善,但是為大家梳理思路的話(huà)就顯得比較冗余,直接貼代碼,先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)開(kāi)始規(guī)范的很清楚,這個(gè)很好判斷
if (event.getY() >= ((mLineHeight * (0 + 0.5) - pieceWidth / 2 + panelHeight))){
//得到XY用于判斷 手指處于哪個(gè)點(diǎn)
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)手指按下去的時(shí)候,我們要判斷按下去的點(diǎn),處于哪一個(gè)大手勢(shì)點(diǎn)范圍內(nèi),并把它加入List<Bean>中。通知onDraw重繪,如上文所說(shuō)的那樣,把點(diǎn)改為選中狀態(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)前手指處于哪個(gè)點(diǎn)范圍內(nèi),如果點(diǎn)沒(méi)存在listData,存進(jìn)去,第一個(gè)點(diǎn)
if (!listDatas.contains(new GestureBean(y, x))) {
listDatas.add(new GestureBean(y, x));
}
}
}
//重繪一次,第一個(gè)點(diǎn)顯示被選中了
invalidate();
break;
(4)MotionEvent.ACTION_MOVE: 手指在View上滑動(dòng),滑動(dòng)到哪個(gè)點(diǎn)就把,哪個(gè)點(diǎn)的坐標(biāo)add到List<Baen>中。在通知重繪
case MotionEvent.ACTION_MOVE:
//手指移動(dòng)在大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) {
//滑倒的店處于哪個(gè)點(diǎn)范圍內(nèi),如果點(diǎn)沒(mé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),會(huì)從loadSharedPrefferenceData獲取到以前的listdatas做比較,判斷是否成功
2、注冊(cè)狀態(tài),會(huì)比較第一次的listdatas,來(lái)判斷兩次驗(yàn)證是否一致,從而,處理成功和失敗的邏輯。
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, "手勢(shì)錯(cuò)誤,還可以再輸入" + tempCount + "次");
listDatas.clear();
}
}
//View處于注冊(cè)狀態(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, "點(diǎn)數(shù)不能小于" + minPointNums + "個(gè)");
invalidate();
return true;
}
listDatasCopy.addAll(listDatas);
listDatas.clear();
mError = false;
AlertUtil.t(mContext, "請(qǐng)?jiān)僖淮卫L制");
} else {
//兩次認(rèn)證成功
if (listDatas.equals(listDatasCopy)) {
saveToSharedPrefference(listDatas);
mError = false;
stateFlag = STATE_LOGIN;
postListener(true);
saveState();
} else {
mError = true;
AlertUtil.t(mContext, "與上次手勢(shì)繪制不一致,請(qǐng)重新設(shè)置");
}
listDatas.clear();
invalidate();
return true;
}
}
invalidate();
break;
至此,手勢(shì)View的所有邏輯大概已經(jīng)清楚了,下面做的是需要完善。
三·、完善View,設(shè)置接口調(diào)用,失敗倒計(jì)時(shí),和關(guān)閉View是清理當(dāng)前SP緩存。
1、仔細(xì)看的朋友會(huì)發(fā)現(xiàn),ACTION.UP中有 postListener(true)這些東西,這就是我定義的接口,來(lái)返回認(rèn)證狀態(tài)。
(1)定義接口,三個(gè)參分別為:view所處狀態(tài),存儲(chǔ)的List,是否成功(注冊(cè)或認(rèn)證)
//定義接口 ,傳遞View狀態(tài)
public interface GestureCallBack{
void gestureVerifySuccessListener(int stateFlag, List<GestureBean> data, boolean success);
}
(2)實(shí)例化接口,當(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í)時(shí)數(shù)據(jù)Action.UP
//給接口傳遞數(shù)據(jù)
private void postListener(boolean success) {
if (gestureCallBack != null) {
gestureCallBack.gestureVerifySuccessListener(stateFlag, listDatas, success);
}
}
2、驗(yàn)證失敗倒計(jì)時(shí),通過(guò)Handler,Timer和TimerTask實(shí)現(xiàn)
(1)、內(nèi)部類(lèi)TimeTask
//定義一個(gè)內(nèi)部TimerTask類(lèi)用于記錄,錯(cuò)誤倒計(jì)時(shí)
static class InnerTimerTask extends TimerTask{
Handler handler;
public InnerTimerTask(Handler handler) {
this.handler = handler;
}
@Override
public void run() {
handler.sendMessage(handler.obtainMessage());
}
}
(2)實(shí)例化,上次錯(cuò)誤時(shí)間也是存儲(chǔ)在SharedPreference,waiTime為定義好的超時(shí)時(shí)間。代碼如下
mTimer = new Timer();
//計(jì)算上次失敗時(shí)間與現(xiàn)在的時(shí)間差
try {
long lastTime = PreferenceCache.getGestureTime();
Date date = new Date();
if (lastTime !=0 && (date.getTime()-lastTime)/1000<waitTime){
//失敗時(shí)間未到,還處于鎖定狀態(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,"請(qǐng)繪制解鎖圖案");
mError = false;
invalidate();
//將計(jì)時(shí)信息還原
reSet();
return;
}
mError = true;
invalidate();
}
};
3、清理手勢(shì)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();
}
//用于更改手勢(shì)密碼,清除以前密碼
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();
}
四、簡(jiǎn)單使用:
1、以設(shè)置手勢(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è)置手勢(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è)置手勢(shì)密碼頁(yè)面):
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)用這個(gè)方法會(huì)造成第二次啟動(dòng)程序直接進(jìn)入手勢(shì)識(shí)別而不是手勢(shì)設(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é)
這個(gè)自定義手勢(shì)密碼,是我參照個(gè)別輪子+我本人的理解,仿照支付寶手勢(shì)密碼設(shè)計(jì)的,整體流暢我已帶大家分析了一波。該View還有很多需要完善的地方,我以后會(huì)慢慢完善,有什么指教或者疑問(wèn),請(qǐng)大家在下面留言。
Demo:
1、手勢(shì)密碼自定義View
2、指紋驗(yàn)證(由于仿支付寶安全的Demo,含指紋我就一塊做了,用的三方)
Demo地址:https://github.com/ChaosOctopus/ChaosSecurityCenter
如果覺(jué)得對(duì)您有用,請(qǐng)給我一個(gè)贊,或者一個(gè)Star。