相信大家在很多的app肯定看到过手势密码解锁View,但是大家有没有想过怎么实现这样一个View,哈,接下来,小编手把手教大家教写一个GesturePasswordView。
先看一张效果图
2018-03-22_12_06_12.gif
要实现这样一个效果,首先需要在屏幕上绘制一个3x3九宫图,如下图
nine.png
具体思路:
1、要知道每个点在屏幕上位置。
2、知道各个点的位置,在去绘制,调用drawCircle(float cx, float cy, float radius, @NonNull Paint paint)方法。*
定义一个Point类,记录下每个点的位置和状态。
public class Point {//点的圆心的x,y的位置public float centerX;public float centerY;//每个点的索引private int index;//正常的状态private int normalState = 0;//按下的状态private int pressState = 1;//错误的状态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的解锁Viewprivate 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的二位数组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; }@Overrideprotected 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); //后绘制内圆 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()) { //设置下线条画笔的颜色 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循环 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九宫图绘制完了,接下来是手指触摸时绘制两个点之间的连线了。如下图
CIJ6_4AYF8Y6YEI3PUOHS7B.png
两个点之间连线是内圆之外与内圆之外的连线,怎么计算这两个点的位置呢?如下图
m9.png
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;@Overridepublic boolean onTouchEvent(MotionEvent event) { //显示错误时有个时间,是错误的状态是手指触摸是不能绘制的 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]; //把所有选中的点的状态设置为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); }
显示错误完毕之后,需要恢复下点的正常状态
/** * 清空所有选中的点 */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]; //把所有选中的点的状态设置为Normal if (mSelectPoints.contains(point)) { point.setNormalState(); } } } mSelectPoints.clear(); }
获取手指触摸的是哪个点,根据点的圆心位置到手指触摸的位置的距离小于外圆的半径。如图
m6.png
只要在蓝色区域内就可以,之外的话手指肯定不在这个圆内。
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的九宫图解锁到这绘制完毕,只粘贴了部分关键代码,完整代码github地址:https://github.com/StevenYan88/GesturePasswordView
有不懂的还可以加小编的微信StevenInSH-,(注意微信号最后面有个减号)。