相信大家在很多的app肯定看到過手勢密碼解鎖View替蔬,但是大家有沒有想過怎么實現(xiàn)這樣一個View告私,哈,接下來承桥,小編手把手教大家教寫一個GesturePasswordView驻粟。
先看一張效果圖
要實現(xiàn)這樣一個效果,首先需要在屏幕上繪制一個3x3九宮圖,如下圖
具體思路:
1蜀撑、要知道每個點在屏幕上位置挤巡。
2、知道各個點的位置酷麦,在去繪制矿卑,調(diào)用drawCircle(float cx, float cy, float radius, @NonNull Paint paint)方法。*
定義一個Point類沃饶,記錄下每個點的位置和狀態(tài)母廷。
public class Point {
//點的圓心的x,y的位置
public float centerX;
public float centerY;
//每個點的索引
private int index;
//正常的狀態(tài)
private int normalState = 0;
//按下的狀態(tài)
private int pressState = 1;
//錯誤的狀態(tài)
private int errorState = 2;
private int state = normalState;
public Point(float centerX, float centerY, int index) {
this.centerX = centerX;
this.centerY = centerY;
this.index = index;
}
public void setErrorState() {
this.state = errorState;
}
public void setPressState() {
this.state = pressState;
}
public void setNormalState() {
this.state = normalState;
}
public void setState(int state) {
this.state = state;
}
public boolean stateIsPress() {
return state == pressState;
}
public boolean stateIsNormal() {
return state == normalState;
}
public boolean stateIsError() {
return state == errorState;
}
GesturePasswordView類
public class GesturePasswordView extends View {
//3x3的解鎖View
private static final int mRow = 3;
private static final int mColumn = 3;
//顏色
private int mNormalColor = Color.GRAY;
private int mPressedColor = Color.BLUE;
private int mErrorColor = Color.RED;
//畫筆
private Paint mNormalPaint;
private Paint mPressPaint;
private Paint mErrorPaint;
private Paint mLinePaint;
//保存Point的二位數(shù)組
private Point[][] mPoints = new Point[3][3];
private float mDotRadius;
//被選中的點
private List<Point> mSelectPoints = new ArrayList<>();
private boolean mInitOnce;
private boolean mIsTouchPoint;
private boolean mIsErrorStatus;
private GesturePasswordViewListener mGesturePasswordViewListener;
public GesturePasswordView(Context context) {
this(context, null);
}
public GesturePasswordView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public GesturePasswordView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void initPaint() {
mNormalPaint = getPaint();
mNormalPaint.setColor(mNormalColor);
mPressPaint = getPaint();
mPressPaint.setColor(mPressedColor);
mErrorPaint = getPaint();
mErrorPaint.setColor(mErrorColor);
mLinePaint = getPaint();
mLinePaint.setColor(mPressedColor);
}
/**
* 畫筆
*
* @return
*/
private Paint getPaint() {
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setDither(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(mDotRadius / 9);
return paint;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!mInitOnce) {
initDot();
initPaint();
mInitOnce = true;
}
for (int i = 0; i < mPoints.length; i++) {
for (int j = 0; j < mPoints[i].length; j++) {
Point point = mPoints[i][j];
if (point.stateIsNormal()) {
//先繪制外圓
canvas.drawCircle(point.centerX, point.centerY, mDotRadius, mNormalPaint);
//后繪制內(nèi)圓
canvas.drawCircle(point.centerX, point.centerY, mDotRadius / 6, mNormalPaint);
} else if (point.stateIsPress()) {
canvas.drawCircle(point.centerX, point.centerY, mDotRadius, mPressPaint);
canvas.drawCircle(point.centerX, point.centerY, mDotRadius / 6, mPressPaint);
} else if (point.stateIsError()) {
//設(shè)置下線條畫筆的顏色
canvas.drawCircle(point.centerX, point.centerY, mDotRadius, mErrorPaint);
canvas.drawCircle(point.centerX, point.centerY, mDotRadius / 6, mErrorPaint);
}
}
}
//繪制兩個點之間的連線
drawLineToCanvas(canvas);
}
/**
* 初始化每個點
*/
private void initDot() {
int width = this.getWidth();
int height = this.getHeight();
int offsetX = 0;
int offsetY = 0;
//兼容下橫豎屏
if (height > width) {
offsetY = (height - width) / 2;
} else {
offsetX = (width - height) / 2;
}
int squareWidth = width / 3;
//外圓的半徑
mDotRadius = width / 12;
//mPoints[0][0] = new Point(offsetX + squareWidth / 2, offsetY + squareWidth / 2, 0);
//mPoints[0][1] = new Point(offsetX + squareWidth * 3 / 2, offsetY + squareWidth / 2, 1);
//mPoints[0][2] = new Point(offsetX + squareWidth * 5 / 2, offsetY + squareWidth / 2, 2);
//mPoints[1][0] = new Point(offsetX + squareWidth / 2, offsetY + squareWidth * 3 / 2, 3);
//mPoints[1][1] = new Point(offsetX + squareWidth * 3 / 2, offsetY + squareWidth * 3 / 2, 4);
//mPoints[1][2] = new Point(offsetX + squareWidth * 5 / 2, offsetY + squareWidth * 3 / 2, 5);
//mPoints[2][0] = new Point(offsetX + squareWidth / 2, offsetY + squareWidth * 5 / 2, 6);
//mPoints[2][1] = new Point(offsetX + squareWidth * 3 / 2, offsetY + squareWidth * 5 / 2, 7);
//mPoints[2][2] = new Point(offsetX + squareWidth * 5 / 2, offsetY + squareWidth * 5 / 2, 8);
//為了簡便糊肤,用for循環(huán)
for (int i = 0; i < mRow; i++) {
for (int j = 0; j < mColumn; j++) {
mPoints[i][j] = new Point(offsetX + squareWidth * (j * 2 + 1) / 2,
offsetY + squareWidth * (i * 2 + 1) / 2, i * mPoints.length + j);
}
}
}
寫到這里3x3九宮圖繪制完了琴昆,接下來是手指觸摸時繪制兩個點之間的連線了。如下圖
兩個點之間連線是內(nèi)圓之外與內(nèi)圓之外的連線馆揉,怎么計算這兩個點的位置呢业舍?如下圖
private void drawLineToCanvas(Canvas canvas) {
if (mSelectPoints.size() >= 1) {
Point lastPoint = mSelectPoints.get(0);
for (int i = 1; i < mSelectPoints.size(); i++) {
drawLine(canvas, lastPoint, mSelectPoints.get(i));
lastPoint = mSelectPoints.get(i);
}
//觸摸的時候繪制
if (mIsTouchPoint) {
drawLine(canvas, lastPoint, new Point(mMovingX, mMovingY, -1));
}
}
}
/**
* 繪制兩個點之間的連線
*
* @param canvas
* @param start
* @param end
*/
private void drawLine(Canvas canvas, Point start, Point end) {
//兩點之間的距離
double pointDistance = MathUtil.distance(end.centerX, end.centerY, start.centerX, start.centerY);
float dx = end.centerX - start.centerX;
float dy = end.centerY - start.centerY;
float rx = (float) ((dx / pointDistance) * (mDotRadius / 6));
float ry = (float) ((dy / pointDistance) * (mDotRadius / 6));
canvas.drawLine(start.centerX + rx, start.centerY + ry,
end.centerX - rx, end.centerY - ry, mLinePaint);
}
處理下手指的Touch事件
float mMovingX;
float mMovingY;
@Override
public boolean onTouchEvent(MotionEvent event) {
//顯示錯誤時有個時間,是錯誤的狀態(tài)是手指觸摸是不能繪制的
if (mIsErrorStatus) {
return false;
}
mMovingX = event.getX();
mMovingY = event.getY();
Point point = getPressPoint();
switch (event.getAction()) {
//手指按下
case MotionEvent.ACTION_DOWN:
if (point != null) {
mIsTouchPoint = true;
mSelectPoints.add(point);
point.setPressState();
}
break;
//手指移動
case MotionEvent.ACTION_MOVE:
if (point != null) {
if (!mSelectPoints.contains(point)) {
mSelectPoints.add(point);
point.setPressState();
}
}
break;
//手指抬起
case MotionEvent.ACTION_UP:
mIsTouchPoint = false;
if (mGesturePasswordViewListener != null) {
if (mSelectPoints.size() < 4) {
showSelectError();
} else {
clearSelectPoints();
}
}
break;
}
invalidate();
return true;
}
用戶可能會解鎖錯誤把介,處理下解鎖View錯誤的情況勤讽,每個點和點與點之間的連線顯示紅色
/**
* 顯示錯誤
*/
private void showSelectError() {
for (int i = 0; i < mPoints.length; i++) {
for (int j = 0; j < mPoints[i].length; j++) {
Point point = mPoints[i][j];
//把所有選中的點的狀態(tài)設(shè)置為Error
if (mSelectPoints.contains(point)) {
point.setErrorState();
mIsErrorStatus = true;
mLinePaint.setColor(mErrorColor);
}
}
}
postDelayed(new Runnable() {
@Override
public void run() {
clearSelectPoints();
mIsErrorStatus = false;
invalidate();
mLinePaint.setColor(mPressedColor);
}
}, 1000);
}
顯示錯誤完畢之后,需要恢復(fù)下點的正常狀態(tài)
/**
* 清空所有選中的點
*/
private void clearSelectPoints() {
for (int i = 0; i < mPoints.length; i++) {
for (int j = 0; j < mPoints[i].length; j++) {
Point point = mPoints[i][j];
//把所有選中的點的狀態(tài)設(shè)置為Normal
if (mSelectPoints.contains(point)) {
point.setNormalState();
}
}
}
mSelectPoints.clear();
}
獲取手指觸摸的是哪個點拗踢,根據(jù)點的圓心位置到手指觸摸的位置的距離小于外圓的半徑脚牍。如圖
只要在藍(lán)色區(qū)域內(nèi)就可以,之外的話手指肯定不在這個圓內(nèi)巢墅。
private Point getPressPoint() {
for (int i = 0; i < mPoints.length; i++) {
for (int j = 0; j < mPoints[i].length; j++) {
Point point = mPoints[i][j];
if (point != null) {
if (MathUtil.checkInRound(point.centerX, point.centerY, mDotRadius, mMovingX, mMovingY)) {
return point;
}
}
}
}
return null;
}
最后說下index的作用诸狭,index是記錄了每個的點的索引,這樣做可以知道用戶連了哪些點君纫。整個3x3的九宮圖解鎖到這繪制完畢驯遇,只粘貼了部分關(guān)鍵代碼,完整代碼github地址:https://github.com/StevenYan88/GesturePasswordView