Android 自定義 View——手勢密碼
Android 自定義 View 當然是十分重要的,筆者這兩天寫了一個自定義 View 的手勢密碼,和大家分享分享:
首先,我們來建立一個表示點的類,Point.java:
public class Point { // 點的三種狀態 public static final int POINT_STATUS_NORMAL = 0; public static final int POINT_STATUS_CLICK = 1; public static final int POINT_STATUS_ERROR = 2; // 預設狀態 public int state = POINT_STATUS_NORMAL; // 點的座標 public float mX; public float mY; public Point(float x,float y){ this.mX = x; this.mY = y; } // 獲取兩個點的距離 public float getInstance(Point a){ return (float) Math.sqrt((mX-a.mX)*(mX-a.mX)+(mY-a.mY)*(mY-a.mY)); } }
然後我們建立一個 HandleLock.java 繼承自 View,並重寫其三種構造方法(不重寫帶兩個引數的構造方法會導致程式出錯):
首先,我們先把後面需要用的變數寫出來,方便大家明白這些變數是幹嘛的:
// 三種畫筆 private Paint mNormalPaint; private Paint mClickPaint; private Paint mErrorPaint; // 點的半徑 private float mRadius; // 九個點,使用二維陣列 private Point[][] mPoints = new Point[3][3]; // 儲存手勢劃過的點 private ArrayList<Point> mClickPointsList = new ArrayList<Point>(); // 手勢的 x 座標,y 座標 private float mHandleX; private float mHandleY; private OnDrawFinishListener mListener; // 儲存滑動路徑 private StringBuilder mRoute = new StringBuilder(); // 是否在畫錯誤狀態 private boolean isDrawError = false;
接下來我們來初始化資料:
// 初始化資料 private void initData() { // 初始化三種畫筆,正常狀態為灰色,點下狀態為藍色,錯誤為紅色 mNormalPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mNormalPaint.setColor(Color.parseColor("#ABABAB")); mClickPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mClickPaint.setColor(Color.parseColor("#1296db")); mErrorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mErrorPaint.setColor(Color.parseColor("#FB0C13")); // 獲取點間隔 float offset = 0; if (getWidth() > getHeight()) { // 橫屏 offset = getHeight() / 7; mRadius = offset / 2; mPoints[0][0] = new Point(getWidth() / 2 - offset * 2, offset + mRadius); mPoints[0][1] = new Point(getWidth() / 2, offset + mRadius); mPoints[0][2] = new Point(getWidth() / 2 + offset * 2, offset + mRadius); mPoints[1][0] = new Point(getWidth() / 2 - offset * 2, offset * 3 + mRadius); mPoints[1][1] = new Point(getWidth() / 2, offset * 3 + mRadius); mPoints[1][2] = new Point(getWidth() / 2 + offset * 2, offset * 3 + mRadius); mPoints[2][0] = new Point(getWidth() / 2 - offset * 2, offset * 5 + mRadius); mPoints[2][1] = new Point(getWidth() / 2, offset * 5 + mRadius); mPoints[2][2] = new Point(getWidth() / 2 + offset * 2, offset * 5 + mRadius); } else { // 豎屏 offset = getWidth() / 7; mRadius = offset / 2; mPoints[0][0] = new Point(offset + mRadius, getHeight() / 2 - 2 * offset); mPoints[0][1] = new Point(offset * 3 + mRadius, getHeight() / 2 - 2 * offset); mPoints[0][2] = new Point(offset * 5 + mRadius, getHeight() / 2 - 2 * offset); mPoints[1][0] = new Point(offset + mRadius, getHeight() / 2); mPoints[1][1] = new Point(offset * 3 + mRadius, getHeight() / 2); mPoints[1][2] = new Point(offset * 5 + mRadius, getHeight() / 2); mPoints[2][0] = new Point(offset + mRadius, getHeight() / 2 + 2 * offset); mPoints[2][1] = new Point(offset * 3 + mRadius, getHeight() / 2 + 2 * offset); mPoints[2][2] = new Point(offset * 5 + mRadius, getHeight() / 2 + 2 * offset); } }
大家可以看到,我來給點定座標是,是按照比較窄的邊的 1/7 作為點的直徑,這樣保證了,不管你怎麼定義 handleLock 的寬高,都可以使裡面的九個點看起來位置很舒服。
接下來我們就需要寫一些函式,將點、線繪製到控制元件上,我自己把繪製分成了三部分,一部分是點,一部分是點與點之間的線,一部分是手勢的小點和手勢到最新點的線。
// 畫點,按照我們選擇的半徑畫九個圓
private void drawPoints(Canvas canvas) {
// 便利所有的點,並且判斷這些點的狀態
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
Point point = mPoints[i][j];
switch (point.state) {
case Point.POINT_STATUS_NORMAL:
canvas.drawCircle(point.mX, point.mY, mRadius, mNormalPaint);
break;
case Point.POINT_STATUS_CLICK:
canvas.drawCircle(point.mX, point.mY, mRadius, mClickPaint);
break;
case Point.POINT_STATUS_ERROR:
canvas.drawCircle(point.mX, point.mY, mRadius, mErrorPaint);
break;
default:
break;
}
}
}
}
// 畫點與點之間的線
private void drawLines(Canvas canvas) {
// 判斷手勢是否已經劃過點了
if (mClickPointsList.size() > 0) {
Point prePoint = mClickPointsList.get(0);
// 將所有已選擇點的按順序連線
for (int i = 1; i < mClickPointsList.size(); i++) {
// 判斷已選擇點的狀態
if (prePoint.state == Point.POINT_STATUS_CLICK) {
mClickPaint.setStrokeWidth(7);
canvas.drawLine(prePoint.mX, prePoint.mY, mClickPointsList.get(i).mX, mClickPointsList.get(i).mY, mClickPaint);
}
if (prePoint.state == Point.POINT_STATUS_ERROR) {
mErrorPaint.setStrokeWidth(7);
canvas.drawLine(prePoint.mX, prePoint.mY, mClickPointsList.get(i).mX, mClickPointsList.get(i).mY, mErrorPaint);
}
prePoint = mClickPointsList.get(i);
}
}
}
// 畫手勢點
private void drawFinger(Canvas canvas) {
// 有選擇點後再出現手勢點
if (mClickPointsList.size() > 0) {
canvas.drawCircle(mHandleX, mHandleY, mRadius / 2, mClickPaint);
}
// 最新點到手指的連線,判斷是否有已選擇的點,有才能畫
if (mClickPointsList.size() > 0) {
canvas.drawLine(mClickPointsList.get(mClickPointsList.size() - 1).mX, mClickPointsList.get(mClickPointsList.size() - 1).mY,
mHandleX, mHandleY, mClickPaint);
}
}
上面的程式碼我們看到需要使用到手勢劃過的點,我們是怎麼選擇的呢?
// 獲取手指移動中選取的點
private int[] getPositions() {
Point point = new Point(mHandleX, mHandleY);
int[] position = new int[2];
// 遍歷九個點,看手勢的座標是否在九個圓內,有則返回這個點的兩個下標
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (mPoints[i][j].getInstance(point) <= mRadius) {
position[0] = i;
position[1] = j;
return position;
}
}
}
return null;
}
我們需要重寫其 onTouchEvent 來通過手勢動作來提交選擇的點,並更新檢視:
// 重寫點選事件
@Override
public boolean onTouchEvent(MotionEvent event) {
// 獲取手勢的座標
mHandleX = event.getX();
mHandleY = event.getY();
int[] position;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
position = getPositions();
// 判斷點下時是否選擇到點
if (position != null) {
// 新增到已選擇點中,並改變其狀態
mClickPointsList.add(mPoints[position[0]][position[1]]);
mPoints[position[0]][position[1]].state = Point.POINT_STATUS_CLICK;
// 儲存路徑,依次儲存其橫縱下標
mRoute.append(position[0]);
mRoute.append(position[1]);
}
break;
case MotionEvent.ACTION_MOVE:
position = getPositions();
// 判斷手勢移動時是否選擇到點
if (position != null) {
// 判斷當前選擇的點是否已經被選擇過
if (!mClickPointsList.contains(mPoints[position[0]][position[1]])) {
// 新增到已選擇點中,並改變其狀態
mClickPointsList.add(mPoints[position[0]][position[1]]);
mPoints[position[0]][position[1]].state = Point.POINT_STATUS_CLICK;
// 儲存路徑,依次儲存其橫縱下標
mRoute.append(position[0]);
mRoute.append(position[1]);
}
}
break;
case MotionEvent.ACTION_UP:
// 重置資料
resetData();
break;
default:
break;
}
// 更新檢視
invalidate();
return true;
}
// 重置資料
private void resetData() {
// 將所有選擇過的點的狀態改為正常
for (Point point :
mClickPointsList) {
point.state = Point.POINT_STATUS_NORMAL;
}
// 清空已選擇點
mClickPointsList.clear();
// 清空儲存的路徑
mRoute = new StringBuilder();
// 不再畫錯誤狀態
isDrawError = false;
}
那我們怎麼繪製檢視呢?我們通過重寫其 onDraw() 方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 判斷是否畫錯誤狀態,畫錯誤狀態不需要畫手勢點已經於最新選擇點的連線
if (isDrawError) {
drawPoints(canvas);
drawLines(canvas);
} else {
drawPoints(canvas);
drawLines(canvas);
drawFinger(canvas);
}
}
那麼這個手勢密碼繪製過程就結束了,但是整個控制元件還沒有結束,我們還需要給它一個監聽器,監聽其繪製完成,選擇後續事件:
private OnDrawFinishListener mListener;
// 定義繪製完成的介面
public interface OnDrawFinishListener {
public boolean drawFinish(String route);
}
// 定義繪製完成的方法,傳入介面
public void setOnDrawFinishListener(OnDrawFinishListener listener) {
this.mListener = listener;
}
然後我們就需要在手勢離開的時候 ,來進行繪製完成時的事件:
case MotionEvent.ACTION_UP:
// 完成時回撥繪製完成的方法,返回比對結果,判斷手勢密碼是否正確
mListener.drawFinish(mRoute.toString());
// 返回錯誤,則將所有已選擇點狀態改為錯誤
if (!mListener.drawFinish(mRoute.toString())) {
for (Point point :
mClickPointsList) {
point.state = Point.POINT_STATUS_ERROR;
}
// 將是否繪製錯誤設為 true
isDrawError = true;
// 重新整理檢視
invalidate();
// 這裡我們使用 handler 非同步操作,使其錯誤狀態保持 0.5s
new Thread(new Runnable() {
@Override
public void run() {
if (!mListener.drawFinish(mRoute.toString())) {
Message message = new Message();
message.arg1 = 0;
handler.sendMessage(message);
}
}
}).run();
} else {
resetData();
}
invalidate();
break;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.arg1) {
case 0:
try {
// 沉睡 0.5s
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 重置資料,並重新整理檢視
resetData();
invalidate();
break;
default:
break;
}
}
};
好了,handleLock,整個過程就結束了,筆者這裡定義了一個監聽器只是給大家提供一種思路,筆者將儲存的大路徑傳給了使用者,是為了保證使用者可以自己儲存密碼,並作相關操作,大家也可以使用 HandleLock 來 儲存密碼,不傳給使用者,根據自己的需求寫出更多更豐富的監聽器,而且這裡筆者在 MotionEvent.ACTION_UP 中直接回調了 drawFinish() 方法,就意味著要使用該 HandleLock 就必須給它設定監聽器。
接下來我們說說 HandleLock 的使用,首先是在佈局檔案中使用:
<com.example.a01378359.testapp.lock.HandleLock
android:id="@+id/handlelock_test"
android:layout_width="match_parent"
android:layout_height="match_parent" />
接下來是程式碼中使用:
handleLock = findViewById(R.id.handlelock_test);
handleLock.setOnDrawFinishListener(new HandleLock.OnDrawFinishListener() {
@Override
public boolean drawFinish(String route) {
// 第一次滑動,則儲存密碼
if (count == 0){
password = route;
count++;
Toast.makeText(LockTestActivity.this,"已儲存密碼",Toast.LENGTH_SHORT).show();
return true;
}else {
// 與儲存密碼比較,返回結果,並且做出相應事件
if (password.equals(route)){
Toast.makeText(LockTestActivity.this,"密碼正確",Toast.LENGTH_SHORT).show();
return true;
}else {
Toast.makeText(LockTestActivity.this,"密碼錯誤",Toast.LENGTH_SHORT).show();
return false;
}
}
}
});
專案地址:原始碼