Android自定義View九宮格手勢(shì)密碼解鎖
由于公司新的項(xiàng)目需要用到九宮格手勢(shì)密碼解鎖的功能,于是覺(jué)得自己寫(xiě)一個(gè)。廢話不多說(shuō),直接上效果圖:

首選我們來(lái)分析下實(shí)現(xiàn)的思路:
1. 繪制出相對(duì)于這個(gè)View的居中的九個(gè)點(diǎn),作為默認(rèn)狀態(tài)的點(diǎn)
2. 點(diǎn)擊屏幕的時(shí)候是否點(diǎn)擊在這九個(gè)點(diǎn)上
3. 在屏幕上滑動(dòng)的時(shí)候,繪制兩個(gè)點(diǎn)之間的線條,以及選中狀態(tài)的點(diǎn)
4. 手指離開(kāi)屏幕的時(shí)候判斷手勢(shì)密碼是否正確,如若錯(cuò)誤這把錯(cuò)誤狀態(tài)下的點(diǎn)和線繪制出來(lái)。
具體實(shí)現(xiàn):
首先我們得繪制出默認(rèn)正常狀態(tài)下的九個(gè)點(diǎn):

/**
* 點(diǎn)的bean
* Created by Administrator on 2015/9/21.
*/
public class Point {
// 正常狀態(tài)
public static final int STATE_NORMAL = 1;
// 按下?tīng)顟B(tài)
public static final int STATE_PRESS = 2;
// 錯(cuò)誤狀態(tài)
public static final int STATE_ERROR = 3;
float x;
float y;
int state = STATE_NORMAL;
public Point(float x, float y){
this.x = x;
this.y = y;
}
/**
* 計(jì)算兩點(diǎn)間的距離
* @param a 另外一個(gè)點(diǎn)
* @return
*/
public float getInstance(Point a){
return (float) Math.sqrt((x-a.x)*(x-a.x) + (y-a.y)*(y-a.y));
}
}
可以看到,給一個(gè)點(diǎn)定義了x、y值以及這個(gè)點(diǎn)的狀態(tài)有三種,默認(rèn)的初始狀態(tài)是正常的狀態(tài)。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
init();// 初始化正常狀態(tài)下的九個(gè)點(diǎn),以及三種狀態(tài)所需要用到的圖片資源
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mErrorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 按下?tīng)顟B(tài)的畫(huà)筆
mPressPaint.setColor(Color.parseColor("#00B7EE"));
mPressPaint.setStrokeWidth(7);
// 錯(cuò)誤狀態(tài)的畫(huà)筆
mErrorPaint.setColor(Color.parseColor("#FB0C13"));
mErrorPaint.setStrokeWidth(7);
// 加載三種狀態(tài)圖片
mNormalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lock_point_normal);
mPressBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lock_point_press);
mErrorBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lock_point_error);
mPointRadius = mNormalBitmap.getWidth() / 2;
// 當(dāng)前視圖的大小
int width = getWidth();
int height = getHeight();
// 九宮格點(diǎn)的偏移量
int offSet = Math.abs(width - height) / 2;
// x、y軸上的偏移量
int offSetX = 0, offSetY = 0;
int pointItemWidth = 0; // 每個(gè)點(diǎn)所占用方格的寬度
if (width > height){ // 橫屏的時(shí)候
offSetX = offSet;
offSetY = 0;
pointItemWidth = height / 4;
}
if (width < height){ // 豎屏的時(shí)候
offSetX = 0;
offSetY = offSet;
pointItemWidth = width / 4;
}
// 初始化九個(gè)點(diǎn)
mPoints[0][0] = new Point(offSetX + pointItemWidth, offSetY + pointItemWidth);
mPoints[0][1] = new Point(offSetX + pointItemWidth * 2, offSetY + pointItemWidth);
mPoints[0][2] = new Point(offSetX + pointItemWidth * 3, offSetY + pointItemWidth);
mPoints[1][0] = new Point(offSetX + pointItemWidth, offSetY + pointItemWidth * 2);
mPoints[1][1] = new Point(offSetX + pointItemWidth * 2, offSetY + pointItemWidth * 2);
mPoints[1][2] = new Point(offSetX + pointItemWidth * 3, offSetY + pointItemWidth * 2);
mPoints[2][0] = new Point(offSetX + pointItemWidth, offSetY + pointItemWidth * 3);
mPoints[2][1] = new Point(offSetX + pointItemWidth * 2, offSetY + pointItemWidth * 3);
mPoints[2][2] = new Point(offSetX + pointItemWidth * 3, offSetY + pointItemWidth * 3);
}
這段代碼要注意的是橫豎屏的偏移量,橫屏的時(shí)候計(jì)算X軸的偏移量,豎屏的時(shí)候計(jì)算Y軸的偏移量。計(jì)算出x y的偏移量后,來(lái)初始化九個(gè)點(diǎn)的位置。我們要讓九宮格的點(diǎn)繪制的位置在 當(dāng)前這個(gè)自定義視圖View的正中間,那么如上圖顯示,第一個(gè)點(diǎn)的起始點(diǎn)就是x = x軸的偏移量 + 格子寬度, y = y軸的偏移量 + 格子寬度。以此可見(jiàn)第二列的點(diǎn)的x值 = 兩個(gè)格子的寬度 + x軸的偏移量,同理第二行的點(diǎn)的y值 = 兩個(gè)格子的寬度 + y周的偏移量。以此類推初始化九個(gè)點(diǎn)的位置。
九個(gè)點(diǎn)的位置初始化后,我們需要來(lái)繪制九個(gè)點(diǎn),這里我用了三種狀態(tài)的圖片來(lái)作為頂點(diǎn)。在init()方法中,初始化了三種bitmap圖片對(duì)象。以及計(jì)算了點(diǎn)的半徑也就是圖片的一半,當(dāng)然我這里的三張圖片大小是一樣的。如果不一樣,還是要重新計(jì)算過(guò)。
接下來(lái)就在onDraw方法里繪制出九個(gè)點(diǎn):
@Override
protected void onDraw(Canvas canvas) {
// 繪制點(diǎn)
drawPoints(canvas);
// 繪制連線
drawLines(canvas);
}
/**
* 繪制所有的點(diǎn)
* @param canvas
*/
private void drawPoints(Canvas canvas){
for (int i = 0; i < mPoints.length; i++){
for (int j = 0; j < mPoints[i].length; j++){
Point point = mPoints[i][j];
// 不同狀態(tài)繪制點(diǎn)
switch (point.state){
case Point.STATE_NORMAL:
canvas.drawBitmap(mNormalBitmap, point.x - mPointRadius, point.y - mPointRadius, mPaint);
break;
case Point.STATE_PRESS:
canvas.drawBitmap(mPressBitmap, point.x - mPointRadius, point.y - mPointRadius, mPaint);
break;
case Point.STATE_ERROR:
canvas.drawBitmap(mErrorBitmap, point.x - mPointRadius, point.y - mPointRadius, mPaint);
break;
}
}
}
}
我們變量初始化好的九個(gè)點(diǎn)對(duì)象的狀態(tài),不同狀態(tài)繪制不同的圖片。這里繪制的時(shí)候要注意初始化點(diǎn)的時(shí)候的x、y值是包括了點(diǎn)圓的半徑的,而繪制圖片又是從左上角開(kāi)始的,所以在繪制的時(shí)候需要減去圖片本身的半徑。
繪制后默認(rèn)的九個(gè)點(diǎn)后,我們接下來(lái)處理手勢(shì)滑動(dòng),覆寫(xiě)onTouchEvent方法:
/**
* 獲取選擇的點(diǎn)的位置
* @return
*/
private int[] getSelectedPointPosition(){
Point point = new Point(mX, mY);
for (int i = 0; i < mPoints.length; i++) {
for (int j = 0; j < mPoints[i].length; j++) {
// 判斷觸摸的點(diǎn)和遍歷的當(dāng)前點(diǎn)的距離是否小于當(dāng)個(gè)點(diǎn)的半徑
if(mPoints[i][j].getInstance(point) < mPointRadius){
// 小于則獲取作為被選中,并返回選中點(diǎn)的位置
int[] result = new int[2];
result[0] = i;
result[1] = j;
return result;
}
}
}
return null;
}
首先我們要判斷手指點(diǎn)擊的位置是否是在點(diǎn)上,獲取屏幕觸摸的點(diǎn)的位置mX、mY,初始化一個(gè)點(diǎn),然后遍歷所有的點(diǎn)與觸摸點(diǎn)的距離 是否 小于 一個(gè)點(diǎn)的圖片的半徑,如果小于表示觸摸的位置在這九個(gè)點(diǎn)中的一個(gè)上。getInstance(point)是計(jì)算兩點(diǎn)之間的距離的方法。公式是:distance = Math.sqrt((x1 - x2)(x1 - x2) + (y1 - y2) (y1 - y2))。如果是觸摸的位置在點(diǎn)上,那就返回這個(gè)點(diǎn)的在九宮格數(shù)組中的下標(biāo)位置。
我們來(lái)看onTouchEvent方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
// 獲取手指觸摸的xy位置
mX = event.getX();
mY = event.getY();
int[] position;
int i, j;
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
// 重置所有的點(diǎn)
resetPoints();
// 獲取選擇的點(diǎn)的位置
position = getSelectedPointPosition();
if (position != null){
isDraw = true; // 標(biāo)記為繪制狀態(tài)
i = position[0];
j = position[1];
mPoints[i][j].state = Point.STATE_PRESS;
// 被選擇的點(diǎn)存入一個(gè)集合中
mSelectedPoints.add(mPoints[i][j]);
mPassPositions.add(i * 3 + j); // 把選中的點(diǎn)的路徑轉(zhuǎn)換成一位數(shù)組存儲(chǔ)起來(lái)
}
break;
case MotionEvent.ACTION_MOVE:
if (isDraw){
position = getSelectedPointPosition();
if (position != null){
i = position[0];
j = position[1];
if (!mSelectedPoints.contains(mPoints[i][j])){
mPoints[i][j].state = Point.STATE_PRESS;
mSelectedPoints.add(mPoints[i][j]);
mPassPositions.add(i * 3 + j);
}
}
}
break;
case MotionEvent.ACTION_UP:
// 標(biāo)記為不在繪制
isDraw = false;
break;
}
// 更新繪制視圖
invalidate();
return true;
}
按下的時(shí)候堅(jiān)持到觸摸的位置就在點(diǎn)上的時(shí)候,就把這個(gè)點(diǎn)的狀態(tài)改成被按下的狀態(tài),同時(shí)存入到mSelectedPoints被選中點(diǎn)的集合中。并標(biāo)記現(xiàn)在是繪制的狀態(tài)下。同樣在移動(dòng)手指的時(shí)候也是把檢測(cè)到的點(diǎn)存儲(chǔ)起來(lái),修改狀態(tài)為按下。當(dāng)手指離開(kāi)屏幕的時(shí)候,把標(biāo)記改為不在繪制。然后通過(guò)invalidate()方法更新視圖(會(huì)去調(diào)用onDraw方法繪制)。
通過(guò)上面的步驟,我們把選中的點(diǎn)都收集了起來(lái),接下來(lái)就是繪制兩個(gè)點(diǎn)連接線:
/**
* 繪制兩點(diǎn)之間的線
* @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, mPressPaint);
}
if (a.state == Point.STATE_ERROR){
canvas.drawLine(a.x, a.y, b.x, b.y, mErrorPaint);
}
}
繪制兩點(diǎn)的連接線比較簡(jiǎn)單,我們只繪制按下和錯(cuò)誤時(shí)候的連線。這是繪制單條連接線的。而我們的手勢(shì)密碼路徑是有多條的,繼續(xù)看:
/**
* 繪制所有的線
* @param canvas
*/
private void drawLines(Canvas canvas){
if (mSelectedPoints.size() > 0){
// 從第一個(gè)被選中的點(diǎn)開(kāi)始繪制
Point a = mSelectedPoints.get(0);
for (int i = 1; i < mSelectedPoints.size(); i++){
Point b = mSelectedPoints.get(i);
drawLine(canvas, a, b); // 連接兩個(gè)點(diǎn)
a = b; // 把下一個(gè)點(diǎn)作為下一次繪制的第一個(gè)點(diǎn)
}
if (isDraw){// 如果還在繪制狀態(tài),那就繼續(xù)繪制連接線
drawLine(canvas, a, new Point(mX, mY));
}
}
}
如果被選中點(diǎn)的集合不是空的,那我們選擇從第一個(gè)被選中點(diǎn)開(kāi)始繪制連接線,遍歷所有被選中點(diǎn)的時(shí)候就要從第二個(gè)點(diǎn)開(kāi)始也就是index為1的時(shí)候,繪制完一個(gè)點(diǎn),就要把下一次繪制連接線的起點(diǎn)改為這一次的連接線的終點(diǎn),也是 a=b;這句的作用。所有被選中的點(diǎn)繪制完后,如果當(dāng)前還處在繪制狀態(tài)(手機(jī)沒(méi)有離開(kāi)屏幕),那我們就new一個(gè)手指觸摸位置作為連接線的終點(diǎn)。好了所有的線都繪制完了,那我們只要在onDraw調(diào)用就好了:
@Override
protected void onDraw(Canvas canvas) {
// 繪制點(diǎn)
drawPoints(canvas);
// 繪制連線
drawLines(canvas);
}
這樣繪制的工作基本就完成了,接下來(lái)我們需要做一個(gè)用來(lái)監(jiān)聽(tīng)手勢(shì)滑動(dòng)完后的接口:
public interface OnDrawFinishedListener{
boolean onDrawFinished(List<Integer> passPositions);
}
回調(diào)的方法里的passPositions是手勢(shì)滑動(dòng)的時(shí)候存儲(chǔ)的九宮格的路徑,對(duì)于九宮格路徑的定義如圖:

在onTouchEvent中,當(dāng)Action動(dòng)作是Up的時(shí)候(手指離開(kāi)屏幕):就會(huì)觸發(fā)手勢(shì)密碼繪制完成的接口:
case MotionEvent.ACTION_UP:
boolean valid = false;
if (mListener != null && isDraw){
// 獲取繪制路徑是否正確
valid = mListener.onDrawFinished(mPassPositions);
}
if (!valid){// 判斷繪制路徑不正確的所有被選中的點(diǎn)的狀態(tài)改為出錯(cuò)
for (Point p: mSelectedPoints){
p.state = Point.STATE_ERROR;
}
}
isDraw = false;
break;
當(dāng)設(shè)置了監(jiān)聽(tīng)接口,并且還處于繪制狀態(tài)的時(shí)候,回調(diào)接口把路徑傳出去給實(shí)現(xiàn)這個(gè)接口的使用者,然后在實(shí)現(xiàn)這個(gè)接口方法的地方判斷和之前設(shè)置存儲(chǔ)的手勢(shì)密碼是否一致,如果不一致返回為false。然后去修改所有的被選中的點(diǎn)的狀態(tài)為錯(cuò)誤的。然后invalidate()去更新視圖。
路徑給出去了,在最初設(shè)定的時(shí)候可以用md5等不可逆的加密方式存儲(chǔ)在手機(jī)中。在需要解鎖的時(shí)候,拿到這個(gè)md5值和解鎖時(shí)候繪制的路徑的md5值做比較就可以了:
// 這個(gè)自定義視圖的使用方法:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.jerry.testproject.ui.ScreenLockActivity">
<com.jerry.testproject.widget.lockview.LockView
android:id="@+id/lockView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#2B2B2B"
/>
</FrameLayout>
LockView lockView = (LockView) findViewById(R.id.lockView);
lockView.setOnDrawFinishedListener(new LockView.OnDrawFinishedListener() {
@Override
public boolean onDrawFinished(List<Integer> passPositions) {
StringBuilder sb = new StringBuilder();
for (Integer i :
passPositions) {
sb.append(i.intValue());
}
// 把字符串md5
String md5Str = MD5Utils.getMD5String(sb.toString());
// 比較路徑是否一致
boolean valid = comparePath(sb.toString());
if (valid){
Toast.makeText(ScreenLockActivity.this, "手勢(shì)密碼正確!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(ScreenLockActivity.this, "手勢(shì)密碼錯(cuò)誤,請(qǐng)重試!", Toast.LENGTH_SHORT).show();
}
return valid;
}
});
至此自定義九宮格手勢(shì)密碼View介紹就結(jié)束了。
下面附上控件的源碼和所用到的資源:Android九宮格手勢(shì)密碼解鎖
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- android 九宮格滑動(dòng)解鎖開(kāi)機(jī)實(shí)例源碼學(xué)習(xí)
- 輕松實(shí)現(xiàn)Android自定義九宮格圖案解鎖
- Android實(shí)現(xiàn)九宮格解鎖
- 輕松實(shí)現(xiàn)安卓(Android)九宮格解鎖
- Android實(shí)現(xiàn)九宮格解鎖的實(shí)例代碼
- 使用Android自定義控件實(shí)現(xiàn)滑動(dòng)解鎖九宮格
- Android 仿小米鎖屏實(shí)現(xiàn)九宮格解鎖功能(無(wú)需圖片資源)
- Android自定義控件實(shí)現(xiàn)九宮格解鎖功能
- Android實(shí)現(xiàn)九宮格手勢(shì)解鎖
- Android自定義控件實(shí)現(xiàn)九宮格解鎖
相關(guān)文章
Android中ImageView.src設(shè)置圖片拉伸、填滿控件的方法
最近公司有個(gè)需求,要展示客戶公司的企業(yè)形象,用一張圖片放在ImageView中實(shí)現(xiàn),但是發(fā)現(xiàn)圖片并沒(méi)有填滿,而是在上下邊上留出了一點(diǎn)空白,下面這篇文章主要跟大家介紹了Android中ImageView.src設(shè)置圖片拉伸、填滿控件的方法,需要的朋友可以參考下。2017-06-06
Android自定義水波紋動(dòng)畫(huà)Layout實(shí)例代碼
這篇文章主要介紹了Android自定義水波紋動(dòng)畫(huà)Layout的實(shí)例代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-11-11
Android自定義view之利用drawArc方法實(shí)現(xiàn)動(dòng)態(tài)效果(思路詳解)
這篇文章主要介紹了Android自定義view之利用drawArc方法實(shí)現(xiàn)動(dòng)態(tài)效果,drawArc方法包含了五個(gè)參數(shù),具體細(xì)節(jié)在本文中給大家提到過(guò),需要的朋友可以參考下2021-08-08
Android實(shí)現(xiàn)Activities之間進(jìn)行數(shù)據(jù)傳遞的方法
這篇文章主要介紹了Android實(shí)現(xiàn)Activities之間進(jìn)行數(shù)據(jù)傳遞的方法,涉及Android中Activities的使用技巧,需要的朋友可以參考下2015-04-04
Android?JetPack組件的支持庫(kù)Databinding詳解
DataBinding是Google發(fā)布的一個(gè)數(shù)據(jù)綁定框架,它能夠讓開(kāi)發(fā)者減少重復(fù)性非常高的代碼,如findViewById這樣的操作。其核心優(yōu)勢(shì)是解決了數(shù)據(jù)分解映射到各個(gè)view的問(wèn)題,在MVVM框架中,實(shí)現(xiàn)的View和Viewmode的雙向數(shù)據(jù)綁定2022-08-08
Android 判斷是開(kāi)發(fā)debug模式,還是發(fā)布release模式的方法
下面小編就為大家?guī)?lái)一篇Android 判斷是開(kāi)發(fā)debug模式,還是發(fā)布release模式的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12
Android ViewPager的MVP架構(gòu)搭建過(guò)程
本文主要介紹了ViewPager在Android中的作用以及使用場(chǎng)景,如引導(dǎo)頁(yè)、圖片瀏覽器、新聞或文章內(nèi)容的多標(biāo)簽頁(yè)等,同時(shí),還詳細(xì)闡述了如何通過(guò)MVP架構(gòu)來(lái)搭建ViewPager,將視圖和邏輯進(jìn)行解耦,提高代碼的可測(cè)試性、可復(fù)用性,使代碼結(jié)構(gòu)更清晰且易于擴(kuò)展功能2024-10-10
JetpackCompose Navigation導(dǎo)航實(shí)現(xiàn)流程
Navigation是Jetpack用于Android導(dǎo)航的組件,作用是處理頁(yè)面跳轉(zhuǎn),以及頁(yè)面跳轉(zhuǎn)過(guò)程中的交互。使用Navigation,你就需要為每個(gè)頁(yè)面設(shè)定一條唯一路徑,它是一個(gè)String常量,形式是DeepLink的樣子,從一個(gè)頁(yè)面跳轉(zhuǎn)到另一個(gè)頁(yè)面,它通過(guò)輸入目的地的路徑進(jìn)行轉(zhuǎn)跳2023-01-01

