幾天前,看到極客學院有一個鎖屏的課程,然後點進去看了看,最後實現了鎖屏,但是最後各個接口並沒有完善。後來自己對此進行了總結並完善相關接口。主要內容就兩點:
1、鎖屏界面的繪製及滑動事件處理;
2、設置鎖屏手勢以及解鎖。
先上效果圖:
打開
錯誤
滑動中
1、鎖屏界面的繪製,這部分我總結爲四個步驟:
1.1 初始化,準備相關的尺寸;
1.2 繪製圓點;
1.3 觸摸事件;
1.4 繪製觸摸事件經過的路徑。
我們先完成這一部分再繼續研究後面的接口回調。
1.1 初始化,準備相關的尺寸。由於這個鎖屏的區域比較複雜,這裏我直接以自定義View來實現的。那麼在View的構造方法裏面,我們需要做什麼準備工作呢?答案是沒有。因爲在初始化的方法中,我們需要拿到具體的尺寸,所以我們直接在OnDraw方法來處理,先上效果圖:(圓形以方形代替了,只要理解這個意義即可,下同)
代碼如下:
/**
* 是否初始化
* inited默認爲false
*/
if(!inited)
{//沒有初始化
inited = true;
init();
}
/**
* 初始化
*/
private void init()
{
/**
* 手指按下的畫筆
*/
pressPaint = new Paint();
/**
* 抗鋸齒
*/
pressPaint.setAntiAlias(true);
/**
* 畫筆顏色
*/
pressPaint.setColor(Color.YELLOW);
/**
* 畫筆線寬
*/
pressPaint.setStrokeWidth(5);
/**
* 提示錯誤的畫筆
*/
errorPaint = new Paint();
errorPaint.setStrokeWidth(5);
errorPaint.setColor(Color.RED);
errorPaint.setAntiAlias(true);
//鎖屏的不同狀態圖片
//正常
bitmapNormal = BitmapFactory.decodeResource(getResources(), R.mipmap.normal);
//按下
bitmapPress = BitmapFactory.decodeResource(getResources(), R.mipmap.press);
//錯誤
bitmapError = BitmapFactory.decodeResource(getResources(),R.mipmap.error);
//格子半徑
//上面三張Bitmap的尺寸是一致的
halfWidth = bitmapError.getWidth()/2;
/**
* 鎖屏寬度
*/
int width = getWidth();
/**
* 鎖屏高度
*/
int height = getHeight();
/**
* 偏移量
* 目的是後面使鎖屏圖片居中
* 值爲款高差的一半
*/
int offet = Math.abs(width-height)/2;
/**
* X軸以及Y軸的偏移量
*/
int offsetx,offsety;
/**
* 格子寬度
*/
int space;
if(width>height)//橫屏的情況
{
space = height/4;
offsetx = offet;
offsety = 0;
}else//豎屏的情況
{
space = width/4;
offsetx = 0;
offsety = offet;
}
// 第一排
points.add(new Point(offsetx+space,offsety+space));
points.add(new Point(offsetx+space*2,offsety+space));
points.add(new Point(offsetx+space*3,offsety+space));
// 第二排
points.add(new Point(offsetx+space,offsety+space*2));
points.add(new Point(offsetx+space*2,offsety+space*2));
points.add(new Point(offsetx+space*3,offsety+space*2));
// 第三排
points.add(new Point(offsetx+space,offsety+space*3));
points.add(new Point(offsetx+space*2,offsety+space*3));
points.add(new Point(offsetx+space*3,offsety+space*3));
}
上面的效果應該知道是什麼意思,但是這個Point的實體可能不是很清楚,這裏我把代碼貼出來:
/**
* 鎖屏的格子
* Created by Vicent on 2016/10/15.
*/
public class Point {
public static final int STATE_NOMAL = 0;
public static final int STATE_PRESS = 1;
public static final int STATE_ERROR = 2;
public float x,y;
public int state;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
/**
* 計算點到改點圓心的距離
* @param a
* @return
*/
public float checkDistance(Point a)
{
return (float)Math.sqrt((x-a.x)*(x-a.x)+(y-a.y)*(y-a.y));
}
}
OK,接下來我們來繼續實現1.2 繪製圓點,這裏每次刷新的時候都需要繪製的。但是對於圓心,我們需要移動一點位置,先看效果圖(在Excel裏面繪製的,大家有沒有比較好的繪製工具呢?我還記得一點AutoCAD,但是怕用這個更麻煩,,,,):
//繪製點
drawPoint(canvas);
/**
* 繪製鎖屏界面的九宮格
* @param canvas
*/
private void drawPoint(Canvas canvas)
{
for (int i = 0;i<points.size();i++)
{
Point point = points.get(i);
switch (point.state)
{
//STATE_NOMAL
case 0:
canvas.drawBitmap(bitmapNormal,point.x-halfWidth,point.y-halfWidth,paint);
break;
//STATE_PRESS
case 1:
canvas.drawBitmap(bitmapPress,point.x-halfWidth,point.y-halfWidth,paint);
break;
//STATE_ERROR
case 2:
canvas.drawBitmap(bitmapError,point.x-halfWidth,point.y-halfWidth,paint);
break;
}
}
}
接下來就應該是1.3觸摸事件了。這部分就只有用代碼說話了,文字表達的話更抽象。代碼如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
mouseX = event.getX();
mouseY = event.getY();
//拿到當前觸摸的點
Point point = getSelctedPoint(mouseX,mouseY);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
//當前的點在屏幕上
if(point!=null)
{
//清除之前的屏幕數據
restPoints();
//需要繪製按下的點
isDraw = true;
//修改狀態
point.state = Point.STATE_PRESS;
//保存當前的點
pointList.add(point);
}
postInvalidate();
break;
case MotionEvent.ACTION_MOVE:
if(isDraw)
{
//當前的點在屏幕上
//當前的點沒有被保存
if(point!=null && !pointList.contains(point))
{
point.state = Point.STATE_PRESS;
pointList.add(point);
}
}
postInvalidate();
break;
case MotionEvent.ACTION_UP:
isDraw = false;
//滑動的點太少了
if(passList.size()<4){
isErr = true;
for (Point p : pointList){
p.state = Point.STATE_ERROR;
}
postInvalidate();
}else{
postInvalidate();
}
break;
}
//當前的滑動事件被消費(處理完畢),不用返回父佈局處理了。
return true;
}
上面調用了的方法: //拿到當前觸摸的點
Point point = getSelctedPoint(mouseX,mouseY);
/**
* 獲取選中的格子
* @param x
* @param y
* @return
*/
private Point getSelctedPoint(float x,float y)
{
Point mousePoint = new Point(x, y);
for (int i=0;i<points.size();i++)
{
//之前的實體類裏面已經寫過checkDistance,這裏就不再贅述了。
if(points.get(i).checkDistance(mousePoint)<halfWidth)
{
return points.get(i);
}
}
return null;
}
接下來就該總結1.4 繪製觸摸事件經過的路徑,跟上面一樣,代碼是世界上最美妙最容易理解的語言,我們還是通過代碼來看最後乾的事情吧!
//繪製滑動過的路徑
if(pointList.size()>0)
{
Point a = pointList.get(0);
for (int i = 1;i<pointList.size();i++)
{
Point b = pointList.get(i);
drawLine(canvas,a,b);
a = b;
}
//滑動的時候繪製經過的每一個點
if(isDraw)
{
drawLine(canvas,a,new Point(mouseX,mouseY));
}
}
//異常狀態的刷新
if(isErr)
refreshErr();
上面調用了前面沒有說到的方法,這裏寫出來看看這些方法是幹什麼的,具體做了什麼事?
/**
* 繪製手勢的直線
* @param canvas
* @param a
* @param b
*/
private void drawLine(Canvas canvas,Point a,Point b)
{
if(a.state == Point.STATE_PRESS)
{
canvas.drawLine(a.x,a.y,b.x,b.y,pressPaint);
}else if(a.state == Point.STATE_ERROR)
{
canvas.drawLine(a.x,a.y,b.x,b.y,errorPaint);
}
}
/**
* 異常狀態下延時更新爲正常狀態
*/
private void refreshErr() {
for (Point p : pointList){
p.state = Point.STATE_NOMAL;
}
//刷新
postInvalidateDelayed(1500);
isErr = false;
}
OK,上面講的索引繪製方法都是在OnDraw方法裏面執行的,現在已經可以正常的繪製了,但也僅僅是繪製,而且根本不能設置手勢的正確與否。那麼,接下來我們就來看看怎麼實現手勢的設置、確認、調用手勢檢查。
由於上面的內容只是一個自定義類(鎖屏View),而後面的內容涉及到自定義View與Activity的通信,所以先看看佈局效果:
既然已經到這裏了,那麼就把xml佈局文件也貼出來,後面說起來也具體一些,不會太抽象。
代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_lock"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 後面的大彩蛋-->
<ImageView
android:id="@+id/lock_laugh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@mipmap/laugh"
android:visibility="gone"/>
<!--鎖屏界面 -->
<LinearLayout
android:id="@+id/lock_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/lock_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="5dp"
android:text="繪製解鎖圖案"/>
<view
android:layout_width="match_parent"
android:layout_height="450dp"
class="cn.com.hhqy.lockapplication.view.GestureLock"
android:id="@+id/lock_view" />
<TextView
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#AAAAAA"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/lock_cancle"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:background="@null"
android:text="取消"/>
<Button
android:id="@+id/lock_sure"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:background="@null"
android:enabled="false"
android:text="繼續"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
這個通信的過程比較複雜,因爲我是自己寫的,沒(不)有(會)使用mvp等設計模式,所以看上去比較凌亂。這也是爲什麼我要總結的原因,因爲我一高興起來自己都不知道這是什麼代碼。。。。。
廢話說完了,接下來我們要繼續總結第二部分內容,即手勢的設置、確認、調用鎖屏的檢查。爲了湊字數還是寫一下:
2.1 手勢的設置;
2.2 手勢的確認;
2.3 調用鎖屏(驗證手勢)
這一部分主要是接口的回調,所以我們需要定義一個接口,這個接口我們可以單獨建一個類,也可以直接放在手勢的View所承載的Activity或者是View本身,不過從規範來講,該類如果需要複用的話最好是放到BaseActivity裏面。這裏因爲基本上不會有複用,所以我們就假巴意思(裝模作樣,四川方言)的寫一個接口類。
2.1 手勢的設置,這裏先建一個設置需要用到的接口,以及手勢設置需要用到的方法。
package cn.com.hhqy.lockapplication;
import java.util.List;
/**
* Activity與GestureLock(鎖屏View)的通信
* Created by Vicent on 2016/11/23.
*/
public interface OnDrawFilshedListener
{
/**
*
* Button設置爲可點擊
* 修改提示語爲“已記錄圖案”
* 修改Button文本爲“重畫”,“繼續”
*/
void OnTouchEventFinshOk1();
/**
* Button1設置爲可點擊
* 修改提示語爲“至少連接4個點,請重畫”
* 修改Button文本爲“重畫”,“繼續”
*/
void OnTouchEventFinshErr1();
/**
* Button設置爲不可點擊
* 修改提示語爲“完成後鬆開手指”
*/
void TouchEventStart();
}
上面的接口已經把需要實現的方法寫得很清楚了,接下來我們需要在GestureLock裏面定義該接口對象,並提供一個公共方法來實例化該對象。雖然很簡單,但是還是忍不住寫一下。。。。。
private OnDrawFilshedListener listener;
public void setOnDrawFilshedListener(OnDrawFilshedListener listener)
{
this.listener = listener;
}
接下來,我們需要在Activity裏面來實現這三個接口方法,將接口裏面的要求給它實現了!不廢話,直接上代碼:
private void initLintener() {
//確認按鈕點擊事件
btnSure.setOnClickListener(this);
//取消按鈕點擊事件
btnCancle.setOnClickListener(this);
//GestureLock的相關接口方法
lockView.setOnDrawFilshedListener(new GestureLock.OnDrawFilshedListener() {
@Override
public void OnTouchEventFinshOk1() {
btnSure.setEnabled(true);
btnCancle.setEnabled(true);
tvHint.setText("已記錄圖案");
btnCancle.setText("重畫");
btnSure.setText("繼續");
}
@Override
public void OnTouchEventFinshErr1() {
btnSure.setEnabled(false);
btnCancle.setEnabled(true);
tvHint.setText("至少連接4個點,請重畫");
btnCancle.setText("重畫");
btnSure.setText("繼續");
}
@Override
public void TouchEventStart() {
btnSure.setEnabled(false);
btnCancle.setEnabled(false);
tvHint.setText("完成後鬆開手指");
}
});
}
Ok,上面我們已經完成了界面的顯示,但是既然是手勢的設置,那麼就需要點擊按鈕來設置。接下來我們看看這個時候按鈕的取消與繼續會執行上面方法?
/**
* 取消鍵響應事件
*/
private void clickCancle() {
lockView.restPoints();
tvHint.setText("繪製解鎖圖案");
btnCancle.setText("取消");
btnSure.setEnabled(false);
}
等等,報告,這裏發現一個坑!
什麼,聽不懂?好吧,我來給你說道說道。你這裏的取消點擊事件怎麼能這麼寫呢?當用戶輸入正確或者錯誤,咋辦呢?還是這樣直接寫嗎?好像不對吧?
其實這裏只是裝逼,爲了把枚舉引出來而已。因爲在我們的邏輯裏面,不管設置手勢的時候正確與否,界面都是這樣顯示的。
不過既然要引出枚舉來,那麼它在這裏有什麼作用啊?其實就是爲後面的狀態做一個區分,告訴Activity,當前還只是設置密碼,後面還有確認密碼、檢查密碼等狀態。說幹就幹,先來看看枚舉我們是怎麼定義的?
private enum State{
Aok/*第一次滑動結果OK*/,
Aerr/*第一次滑動結果Err*/,
Bok/*第二次滑動結果OK*/,
Berr/*第二次滑動結果Err*/,
};
該State對象默認爲空,接下來看看取消鍵響應事件的內容:
/**
* 取消鍵響應事件
*/
private void clickCancle() {
if(state==null)
finish();
if (state== State.Aerr || state== State.Aok){
lockView.restPoints();
tvHint.setText("繪製解鎖圖案");
btnCancle.setText("取消");
btnSure.setEnabled(false);
state = null;
}
}
看到這裏可能會疑問這個狀態是什麼時候賦值,其實應該是在接口的回調裏面來判斷當前的狀態,這裏重寫之前的接口回調方法。
lockView.setOnDrawFilshedListener(new GestureLock.OnDrawFilshedListener() {
@Override
public void OnTouchEventFinshOk1() {
btnSure.setEnabled(true);
btnCancle.setEnabled(true);
tvHint.setText("已記錄圖案");
btnCancle.setText("重畫");
btnSure.setText("繼續");
state = State.Aok;
}
@Override
public void OnTouchEventFinshErr1() {
btnSure.setEnabled(false);
btnCancle.setEnabled(true);
tvHint.setText("至少連接4個點,請重畫");
btnCancle.setText("重畫");
btnSure.setText("繼續");
state = State.Aerr;
}
@Override
public void TouchEventStart() {
btnSure.setEnabled(false);
btnCancle.setEnabled(false);
tvHint.setText("完成後鬆開手指");
state = null;
}
});
OK,接下來我們看看2.1的重點,設置手勢。這裏我們需要在確認鍵響應事件裏面來完成。我們看看這裏完成了什麼具體的內容?
/**
* 確認鍵響應事件
*/
private void clickSure() {
if(state== State.Aok){
tvHint.setText("再次繪製圖案以確認");
btnCancle.setText("取消");
btnSure.setText("確認");
btnSure.setEnabled(false);
state = null;
lockView.clickGoOn();
}
}
這裏不要問我爲什麼第一次手勢錯誤(比如滑動的時候只經歷了3個點,默認必須達到4個點)的狀態爲什麼不幹事情,是不是偷懶了?我只能告訴你繼續看看前面的代碼,錯誤的時候確認鍵是禁止點擊的。
接下來我們需要到GestureLock裏面去看看這個clickGoOn方法是幹嘛的?他是怎麼設置的?
/**
* 點擊繼續按鈕的響應
*/
public void clickGoOn(){
//默認值爲0
state = 2;
//保存剛剛滑動的時候,滑動的點及順序
checkNumner.addAll(passList);
//重置參數並刷新界面待第二次確認手勢
restPoints();
}
看到這裏可能會有疑問:這個state是幹嘛的?什麼時候用?怎麼用?第二是那個restPoints方法是怎麼實現的?這裏我們先看看方法的內容:
/**
* 重置參數以及刷新界面
*/
public void restPoints()
{
//滑動手勢經歷的點的順序及數量
passList.clear();
//滑動的點
pointList.clear();
for (int i = 0;i<points.size();i++)
{
points.get(i).state = Point.STATE_NOMAL;
}
isErr = false;
postInvalidate();
}
這個方法比較簡單,就不解釋了。接下來看看這個state是在什麼地方怎麼用的?其實他只是在手指擡起界面的時候來判斷是,我們現在在來看看這部分代碼:
case MotionEvent.ACTION_UP:
isDraw = false;
if(state==0){
if(passList.size()<4){
isErr = true;
for (Point p : pointList){
p.state = Point.STATE_ERROR;
}
postInvalidate();
if(listener!=null)
listener.OnTouchEventFinshErr1();
}else{
postInvalidate();
if(listener!=null)
listener.OnTouchEventFinshOk1();
}
}else{
/***/
}
break;
唉呀媽呀,終於把這一部分寫清楚了。接下來繼續第二坑,2.2 手勢的確認。其實嚴格來講,這一部分和2.1應該是一個整體的。但是爲了上面說得順口,就分成了兩部分。這樣也有一個好處,就是每種狀態比較清楚了。廢話少說,我們直接在接口裏面增加第二次滑動手勢的時候接口回調方法。
/**
* Activity與GestureLock(鎖屏View)的通信
* Created by Vicent on 2016/11/23.
*/
public interface OnDrawFilshedListener
{
。。。
/**
* Button1設置爲可點擊
* 修改提示語爲“圖案錯誤,請重試”
* 修改Button文本爲“取消”,“確認”
*/
void OnTouchEventFinshErr2();
/**
* Button設置爲可點擊
* 修改提示語爲“你的新解鎖圖案”
* 修改Button文本爲“取消”,“確認”
* @param number
*/
void OnTouchEventFinshOk2(List<Integer> number);
。。。
}
既然這裏也已經寫好了我們需要實現的內容,那麼我們先實現了接口方法再繼續看下面的內容。
lockView.setOnDrawFilshedListener(new GestureLock.OnDrawFilshedListener() {
@Override
public void OnTouchEventFinshErr2() {
btnSure.setEnabled(false);
btnCancle.setEnabled(true);
tvHint.setText("圖案錯誤,請重試");
btnCancle.setText("取消");
btnSure.setText("確認");
state = State.Berr;
}
@Override
public void OnTouchEventFinshOk2(List<Integer> number) {
btnSure.setEnabled(true);
btnCancle.setEnabled(true);
tvHint.setText("你的新解鎖圖案");
btnCancle.setText("取消");
btnSure.setText("確認");
state = State.Bok;
//手勢經歷的點的順序及數量
passwordNumber = number;
}
});
接下來我們需要關心的是上面的兩個接口方法在什麼情況下如何調用的?這裏我們還得回到GestureLock的OnTouchEvent方法:
case MotionEvent.ACTION_UP:
isDraw = false;
if(state==0){
if(passList.size()<4){
isErr = true;
for (Point p : pointList){
p.state = Point.STATE_ERROR;
}
postInvalidate();
if(listener!=null)
listener.OnTouchEventFinshErr1();
}else{
postInvalidate();
if(listener!=null)
listener.OnTouchEventFinshOk1();
}
}else{
if(passList.size()<4 || checkNumber()){
isErr = true;
for (Point p : pointList){
p.state = Point.STATE_ERROR;
}
postInvalidate();
if(listener!=null){
listener.OnTouchEventFinshErr2();
}
}else{
postInvalidate();
if(listener!=null)
listener.OnTouchEventFinshOk2(passList);
}
}
break;
}
其實這裏就是在第二次觸摸的時候增加了一個判斷是否爲錯誤手勢的標準,即checkNumber方法,該方法返回true則該手勢爲錯誤手勢。我們進入這個手勢裏面去看看這裏是怎麼來判斷的?
/**
* 確認九宮格密碼是否錯誤
* @return
*/
private boolean checkNumber() {
//當前經歷的點和設置的點數量是否一致
if(passList.size()!=checkNumner.size())
return true;
for (int i = 0; i < checkNumner.size(); i++) {
//每個點的順序是否一致
if (passList.get(i)!=checkNumner.get(i))
return true;
}
return false;
}
OK,這裏我們已經完成了手勢的確認,並返回了手勢經歷點的順序及數量。這裏我們還要完善兩個方法:
/**
* 確認鍵響應事件
*/
private void clickSure() {
if(state== State.Aok){
tvHint.setText("再次繪製圖案以確認");
btnCancle.setText("取消");
btnSure.setText("確認");
btnSure.setEnabled(false);
state = null;
lockView.clickGoOn();
}else if(state == State.Bok){
//TODO 保存順序
if(passwordNumber!=null){
//保存手勢數據
saveArray();
//TODO 手勢設置好了,打算幹什麼?
startActivity(new Intent(this,MainActivity.class));
//關閉
finish();
}
}
}
這裏插入一個方法,該方法是通過SharedPreferences保存List數據,方法也挺簡單:
/**
* 保存List數據
* @return
*/
public void saveArray() {
SharedPreferences.Editor mEdit1 = sp.edit();
mEdit1.putInt("Status_size",passwordNumber.size()); /*sKey is an array*/
for(int i=0;i<passwordNumber.size();i++) {
mEdit1.remove("Status_" + i);
mEdit1.putInt("Status_" + i, passwordNumber.get(i));
}
mEdit1.apply();
}
然後完善第二個需要完善的方法:
/**
* 取消鍵響應事件
*/
private void clickCancle() {
if(state==null)
finish();
if (state== State.Aerr || state== State.Aok){
lockView.restPoints();
tvHint.setText("繪製解鎖圖案");
btnCancle.setText("取消");
btnSure.setEnabled(false);
state = null;
}else if(state== State.Berr || state == State.Bok ){
finish();
}
}
到這裏我們就完成了2.2 手勢的確認。最後一個任務是2.3 調用鎖屏(驗證手勢)。這個任務其實很簡單,我們直接在Activity的OnCreate方法裏面即可判斷當前是否需要調用驗證手勢的功能,只需要調用一個initTwice方法即可。方法內容如下:
/**
* 判斷是否解鎖初始化
*/
private void initTwice() {
//加載手勢數據
loadArray();
if(passwordNumber.size()!=0){
setTitle("繪製圖案以解鎖");
btnSure.setEnabled(true);
btnCancle.setEnabled(true);
btnSure.setText("忘記圖案");
//切換當前的狀態
state = State.Twice;
lockView.setTwice(passwordNumber);
}
}
這裏我們又調用了一個GestureLock的setTwice方法,來設置驗證手勢,接下來我們再去分析這個方法具體做了什麼?
/**
* 驗證手勢
* @param password
*/
public void setTwice(List<Integer> password){
checkNumner.clear();
checkNumner = password;
state = 4;
}
上面我們將密碼傳了過去,然後將GestureLock的狀態設置爲驗證手勢的狀態,接下來我們一樣的需要到接口裏面增加相關的兩個方法,即手勢錯誤與正確的時候界面作何反應?
/**
* 驗證手勢時連續輸錯五次
* 禁止解鎖30秒
*/
void OnError5Times();
/**
* 當手勢驗證通過
*/
void OnTouchCheckOk();
既然這裏寫了這幾個方法,那麼就需要我們去實現接口方法,否則Activity會一直報錯。
鎖屏的實現:
@Override
public void OnError5Times() {
//攔截滑動事件,禁止滑動
lock.getParent().requestDisallowInterceptTouchEvent(false);
//禁止點擊
btnSure.setEnabled(false);
//開啓定時器
timer.schedule(new TimerTask() {
@Override
public void run() {
handler.sendEmptyMessage(0);
}
},0,1000);
}
解鎖通過:
@Override
public void OnTouchCheckOk() {
//TODO 待執行相關事宜
ivLaugh.setImageResource(R.mipmap.fly);
ivLaugh.setVisibility(View.VISIBLE);
isForget = false;
state = null;
}
上面看到在鎖屏過程中,我們通過計時器Timer來計時的,那這裏發送消息後又坐了什麼呢?打開看一看?
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if(msg.what==0){
tvHint.setText("請"+stopTouchEventTimes+"秒後重試");
//減時
stopTouchEventTimes--;
if(stopTouchEventTimes==0){
//取消滑動事件攔截(取消鎖屏)
lockView.getParent().requestDisallowInterceptTouchEvent(true);
//取消定時器
timer.cancel();
btnSure.setEnabled(true);
stopTouchEventTimes = 25;
return;
}
}
}
};
現在弄明白了接口方法是怎麼實現的,那接下來就看看接口在什麼地方調用的呢?老規矩,還是在手勢擡起來的時候,一言不合就上代碼:
case MotionEvent.ACTION_UP:
isDraw = false;
if(state==0){
。。。
}
else if(state==4){
if(passList.size()<4 || checkNumber()){
isErr = true;
for (Point p : pointList){
p.state = Point.STATE_ERROR;
}
postInvalidate();
errTimes++;
if(errTimes==5){
if(listener!=null)
listener.OnError5Times();
errTimes = 0;
}
}else{
postInvalidate();
if(listener!=null)
listener.OnTouchCheckOk();
state = 0;
}
}
break;
上面有一個errTimes來對錯誤次數計數,默認爲0。當錯誤次數達到5次就接口的鎖屏方法——OnError5Times方法,調用之後將錯誤次數重新重置爲0 ,如果在驗證手勢的時候成功了,同樣是需要把該次數重置爲0。
接下來我們可以測試一下接口調用的三個方法。
設置鎖屏手勢:
確認鎖屏手勢:
驗證手勢錯誤:
驗證手勢正確:
怎麼樣,可以吧?
小心,這裏還有大坑!你試一試下面的這組手勢看看是不是也可以“開心到飛起”?
估計測試以後你也不能開心到飛起了,因爲現在這個手勢只能驗證滑動經歷的路徑(點)長度(數量),不能驗證路徑位置(點的順序)。那麼我們這裏又需要重頭來研究我們是怎麼記錄順序的?(好像前頭跳過去了沒有講。。。。。)
@Override
public boolean onTouchEvent(MotionEvent event) {
mouseX = event.getX();
mouseY = event.getY();
//拿到當前觸摸的點
Point point = getSelctedPoint(mouseX,mouseY);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
//當前的點在屏幕上
if(point!=null)
{
//清除之前的屏幕數據
restPoints();
//需要繪製按下的點
isDraw = true;
//修改狀態
point.state = Point.STATE_PRESS;
//保存當前的點
pointList.add(point);
passList.add(pointList.size());
if(listener!=null){
listener.TouchEventStart();
}
}
postInvalidate();
break;
case MotionEvent.ACTION_MOVE:
if(isDraw)
{
if(point!=null && !pointList.contains(point))
{
point.state = Point.STATE_PRESS;
pointList.add(point);
passList.add(pointList.size();
}
}
postInvalidate();
break;
...
}
}
前面我們記錄路徑的位置,是通過路徑的點來判斷的,這個方法只能記錄數量,並不能準確的記錄點的順序。所以我們需要對點增加標記位,記錄點相對於View的位置,然後在每個滑動的時候獲得不爲空的點,就單獨記錄這個點位於整個View的位置。
首先修改實體類:
/**
* 鎖屏的格子
* Created by Vicent on 2016/10/15.
*/
public class Point {
...
public int index = -1;
public Point(float x, float y, int index) {
this.x = x;
this.y = y;
this.index = index;
}
}
然後在初始化每個點的時候對每個點的位置進行標註:
// 第一排
points.add(new Point(offsetx+space,offsety+space,0));
points.add(new Point(offsetx+space*2,offsety+space,1));
points.add(new Point(offsetx+space*3,offsety+space,2));
// 第二排
points.add(new Point(offsetx+space,offsety+space*2,3));
points.add(new Point(offsetx+space*2,offsety+space*2,4));
points.add(new Point(offsetx+space*3,offsety+space*2,5));
// 第三排
points.add(new Point(offsetx+space,offsety+space*3,6));
points.add(new Point(offsetx+space*2,offsety+space*3,7));
points.add(new Point(offsetx+space*3,offsety+space*3,8));
然後在記錄路徑的位置時,我們記錄各個點的index值,來確保每個點在View的位置是唯一的,記錄方法修改爲:
passList.add(point.index);
接下來再來測試一下?
現在又可以開心得飛起來吧了!!
不要以爲完了,這裏還有一個大坑!就是我們對於事件攔截的用法測試的時候你會發現完全不起作用?這是爲什麼呢?
先看看我們這個方法的源碼:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
其實這個方法我也沒有怎麼看懂,反正主要就是對標誌位FLAG_DISALLOW_INTERCEPT進行賦值,因爲在事件分發機制的dispatchTouchEvent方法會檢查該標記位,源碼:
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
這部分內容較多,主要是MotionEvent爲ACTION_DOWN或者滑動事件沒有被消費的情況下,會執行後面的方法,這裏可以當做郵局(ViewGroup)收到郵件(手勢)後,判斷這個郵件是不是需要發往外地(攔截),如果需要就進入了方法內,這裏就會檢查標記位是否被設置(地址是不是本地的?),如果設置了的話就會判斷是否可不發往外地(是否可攔截);沒有設置的話直接發出去(不攔截)。
如果看了上面的還是沒有看懂也沒有關係,我就直接摘抄一段《Android開發藝術探索》的一部分內容:
147頁:在子View設置FLAG_DISALLOW_INTERCEPT標記位後,ViewGroup將無法攔截除了ACTION_DOWN以外的其它事件,原因爲ViewGroup在事件分發時,如果是 ACTION_DOWN就會重置FLAG_DISALLOW_INTERCEPT標記位;
160頁,內部攔截法典型代碼解釋部分:除了子元素需要處理以外,父元素也要默認攔截除了ACTION_DOWN以外的其它事件,這樣子當子元素調用getParent().requestDisallowInterceptTouchEvent(false)方法時,父元素才能攔截所需要的事件。
OK,這裏已經解釋了爲什麼這個getParent().requestDisallowInterceptTouchEvent方法在本例中失效且不適用的問題,因爲我們不能在設置手勢密碼或者驗證手勢密碼的時候不能攔截ACTION_MOVE等事件。如果還是不清楚的話,只有多看看上面提到的兩處源碼了!
既然這個方法不能用,我們也知道了事件分發機制(就是三個方法,dispatchTouchEvent方法,onInterceptTouchEvent方法和onTouchEvent方法,這裏假裝講了)。那我們可以不在父佈局的攔截和分發處動手,因爲這兩個方法是私有方法,需要自定義父佈局,代碼量太多了。我們直接在View的onTouchEvent方法來實現攔截,具體實現方法如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
mouseX = event.getX();
mouseY = event.getY();
//判斷是否攔截該事件
//這裏只會攔截ACTION_DOWN,原因是其它事件根本過不來直接被父佈局攔截了
if(!isInterceptTouchEvent){
return isInterceptTouchEvent;
}
......
}
OK,現在終於把坑填滿了!!
最後有一個疑問:如何實現滑動解鎖的時候經歷的這個點實現放大縮小的動畫?求大神指教!!
最最後的一個疑問:如何通過設計模式,使得該demo思路更加清晰?比如說MVP。
求大神給予指導!!
源碼點擊下載(由於這個是我的一個demo集合,具體包名:cn.com.vicent.demos.lockscreen 類名:LockScreenActivity)