package com.hdl.widget.gesturelock; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.RelativeLayout; import com.hdl.widget.HDLUtlisXM; import java.util.ArrayList; import java.util.List; /** * Created by JLChen on 2019/10/30 */ public class GestureLockView extends RelativeLayout { private static final String TAG = "GestureLockView"; public static int DEFAULT_SELECT_COLOR = 0xFFFC744B; //选中颜色 public static int DEFAULT_BORDER_COLOR = 0xFF818181; //边框 // public static int DEFAULT_BACK_TRUE_COLOR = 0x20FC744B; //抬起正确背景颜色 // public static int DEFAULT_BACK_FALSE_COLOR = 0xFFEDACA7; //抬起错误背景颜色 public static int DEFAULT_SELECT_FALSE_COLOR = 0xFFFF0000; //错误红颜色 //模式选择,重置密码,设置密码模式 public static final int RESET_MODE = 0; //验证密码模式 public static final int VERIFY_MODE = 1; // 关于LockView的边长(n*n): n * mLockViewWidth + ( n + 1 ) * mLockViewMargin = mWidth private int mLockViewWidth = 0; //mLockViewWidth * 0.25 private int mLockViewMargin = 0; //LockView数组 private ArrayList mILockViews = new ArrayList<>(1); private LockViewFactory mLockViewFactory = null; //x*x的手势解锁 private int mDotCount = 3; //画笔 private Paint mPaint; //路径 private Path mPath; //连接线的宽度 private float mStrokeWidth = 2; //手指触摸是,path颜色 private int mFingerTouchColor = DEFAULT_SELECT_COLOR; //手指抬起时,密码匹配path颜色 private int mFingerUpMatchedColor = DEFAULT_SELECT_COLOR; //手指抬起时,密码不匹配path颜色 private int mFingerUpUnmatchedColor = DEFAULT_SELECT_FALSE_COLOR; //path上一次moveTo到的点坐标 private float mLastPathX = 0; private float mLastPathY = 0; //指引线的终点坐标 private float mLineX = 0; private float mLineY = 0; //保存选中的LockView id private ArrayList mChooseList = new ArrayList<>(1); //答案list private ArrayList mAnswerList = new ArrayList<>(1); //是否可以触摸 private boolean mTouchable = true; // //允许的尝试次数 // private int mTryTimes = 5; // //保存的尝试次数,因为模式切换的时候TryTimes可能不等于初始设置的值 // private int mSavedTryTimes = 5; private OnLockVerifyListener mOnLockVerifyListener; private OnLockResetListener mOnLockResetListener; //当前模式 private int mCurrentMode = VERIFY_MODE; //RESET_MODE下最少连接数 private int mMinCount = 3; private int resetDelayMillis = 300; public GestureLockView(Context context) { this(context, null); } public GestureLockView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } private void init(final Context context) { if (mLockViewFactory == null) { setLockView(new LockViewFactory() { @Override public ILockView newLockView() { return new QQLockView(context); } }); } mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(HDLUtlisXM.dp2px(context, mStrokeWidth)); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStrokeJoin(Paint.Join.ROUND); mPath = new Path(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); width = width > height ? height : width; mLockViewWidth = (int) (4 * width * 1.0f / (5 * mDotCount + 1)); //计算LockView的间距 mLockViewMargin = (int) (mLockViewWidth * 0.25); if (mLockViewFactory != null) { setLockViewParams(mLockViewFactory); } } /** * 设置LockView的参数并添加到布局中 * * @param lockViewFactory */ private void setLockViewParams(LockViewFactory lockViewFactory) { if (mILockViews.size() > 0) { return; } for (int i = 0; i < mDotCount * mDotCount; i++) { ILockView iLockView = lockViewFactory.newLockView(); iLockView.getView().setId(i + 1); mILockViews.add(iLockView); RelativeLayout.LayoutParams lockerParams = new LayoutParams(mLockViewWidth, mLockViewWidth); //不是每行的第一个,则设置位置为前一个的右边 if (i % mDotCount != 0) { lockerParams.addRule(RelativeLayout.RIGHT_OF, mILockViews.get(i - 1).getView().getId()); } //从第二行开始,设置为上一行同一位置View的下面 if (i > mDotCount - 1) { lockerParams.addRule(RelativeLayout.BELOW, mILockViews.get(i - mDotCount).getView().getId()); } //设置右下左上的边距 int rightMargin = mLockViewMargin; int bottomMargin = mLockViewMargin; int leftMargin = 0; int topMargin = 0; //每个View都有右外边距和底外边距 第一行的有上外边距 第一列的有左外边距 if (i >= 0 && i < mDotCount) {//第一行 topMargin = mLockViewMargin; } if (i % mDotCount == 0) {//第一列 leftMargin = mLockViewMargin; } lockerParams.setMargins(leftMargin, topMargin, rightMargin, bottomMargin); mILockViews.get(i).onNoFinger(); mILockViews.get(i).getView().setLayoutParams(lockerParams); addView(mILockViews.get(i).getView()); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); } @Override protected void onFinishInflate() { super.onFinishInflate(); } /** * 保存状态 * * @return */ @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); // ss.tryTimes = mTryTimes; return ss; } /** * 恢复状态 * * @param state */ @Override protected void onRestoreInstanceState(Parcelable state) { SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); // mTryTimes = savedState.tryTimes; } @Override public boolean onTouchEvent(MotionEvent event) { if (mTouchable) { int action = event.getAction(); int x = (int) event.getX(); int y = (int) event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: handleDownEvent(x, y); break; case MotionEvent.ACTION_MOVE: handleMoveEvent(x, y); break; case MotionEvent.ACTION_UP: handleUpEvent(); break; } invalidate(); return true; } else { return false; } } /** * 处理按下事件 * * @param x * @param y */ private void handleDownEvent(int x, int y) { reset(); handleMoveEvent(x, y); } /** * 处理移动事件 * * @param x * @param y */ private void handleMoveEvent(int x, int y) { mPaint.setColor(mFingerTouchColor); ILockView lockView = getLockViewByPoint(x, y); if (lockView != null) { int childId = lockView.getView().getId(); if (!mChooseList.contains(childId)) { mChooseList.add(childId); lockView.onFingerTouch(); //手势解锁监听 if (mOnLockVerifyListener != null) { mOnLockVerifyListener.onGestureSelected(childId); } mLastPathX = lockView.getView().getLeft() / 2 + lockView.getView().getRight() / 2; mLastPathY = lockView.getView().getTop() / 2 + lockView.getView().getBottom() / 2; if (mChooseList.size() == 1) { mPath.moveTo(mLastPathX, mLastPathY); } else { mPath.lineTo(mLastPathX, mLastPathY); } } } //指引线终点坐标 mLineX = x; mLineY = y; } /** * 处理抬起事件 */ private void handleUpEvent() { if (mCurrentMode == RESET_MODE) { handleResetMode(); } else { handleVerifyMode(); } //将指引线的终点坐标设置为最后一个Path的原点,即取消指引线 mLineX = mLastPathX; mLineY = mLastPathY; } /** * 处理修改密码模式 */ private void handleResetMode() { if (mAnswerList.size() <= 0) { //如果AnswerList.size()==0则为第一次设置,验证连接数 if (mChooseList.size() < mMinCount) { //连接数不符 if (mOnLockResetListener != null) { mOnLockResetListener.onConnectCountUnmatched(mChooseList.size(), mMinCount); } toggleLockViewMatchedState(false); return; } else { //连接数符合,将选择的答案赋值给mAnswerList for (Integer integer : mChooseList) { //因为mAnswerList是从0开始,chooseList保存的是id从1开始,所以-1 mAnswerList.add(integer - 1); } if (mOnLockResetListener != null) { mOnLockResetListener.onFirstPasswordFinished(mAnswerList); } toggleLockViewMatchedState(true); } } else { //mAnswerList已有答案,则验证密码,两次密码匹配保存密码 boolean isAnswerRight = checkAnswer(); if (isAnswerRight) { //两次密码正确,回调 toggleLockViewMatchedState(true); if (mOnLockResetListener != null) { mOnLockResetListener.onSetPasswordFinished(true, mAnswerList); } } else { //两次没密码不正确 toggleLockViewMatchedState(false); if (mOnLockResetListener != null) { mOnLockResetListener.onSetPasswordFinished(false, new ArrayList(1)); } } } } // /** // * 处理验证密码模式 // */ // private void handleVerifyMode() { // mTryTimes--; // boolean isAnswerRight = checkAnswer(); // //手势解锁监听 // if (mOnLockVerifyListener != null) { // mOnLockVerifyListener.onGestureFinished(isAnswerRight); // if (mTryTimes <= 0) { // mOnLockVerifyListener.onGestureTryTimesBoundary(); // } // } // if (!isAnswerRight) { // toggleLockViewMatchedState(false); // } else { // toggleLockViewMatchedState(true); // } // //// resetGestureTimer(); // } /** * 处理验证密码模式 */ private void handleVerifyMode() { //手势解锁监听 if (mOnLockVerifyListener != null) { mOnLockVerifyListener.onGestureFinished(getAnswer(), mChooseList.size()); } } private void resetGestureTimer(){ postDelayed(new Runnable() { @Override public void run() { resetGesture(); } }, resetDelayMillis); } /** * 检查x,y点是否在LockView中 * * @param childView * @param x * @param y * @return */ private boolean checkPointInChild(View childView, int x, int y) { //设置了内边距,即x,y必须落入下GestureLockView的内部中间的小区域中,可以通过调整padding使得x,y落入范围不变大,或者不设置padding int padding = (int) (mLockViewWidth * 0.1); if (x >= childView.getLeft() + padding && x <= childView.getRight() - padding && y >= childView.getTop() + padding && y <= childView.getBottom() - padding) { return true; } return false; } /** * 同过x,y点获取LockView对象 * * @param x * @param y * @return */ private ILockView getLockViewByPoint(int x, int y) { for (ILockView lockView : mILockViews) { if (checkPointInChild(lockView.getView(), x, y)) { return lockView; } } return null; } /** * 重置手势解锁 */ private void reset() { if (mChooseList == null || mPath == null || mILockViews == null) { return; } mChooseList.clear(); mPath.reset(); for (ILockView iLockView : mILockViews) { iLockView.onNoFinger(); } } /** * 重置手势 */ public void resetGesture() { reset(); invalidate(); } /** * 检查答案是否正确 * * @return */ private boolean checkAnswer() { if (mAnswerList.size() != mChooseList.size()) { return false; } for (int i = 0; i < mAnswerList.size(); i++) { if (mAnswerList.get(i) != mChooseList.get(i) - 1) { return false; } } return true; } /** * 检查答案是否正确 * * @return */ private String getAnswer() { String pas = ""; if(mChooseList != null) { for (int mChooseId : mChooseList) { pas += mChooseId; } } return pas; } /** * 切换LockView是否匹配状态 * * @param isMatched */ private void toggleLockViewMatchedState(boolean isMatched) { if (isMatched) { mPaint.setColor(mFingerUpMatchedColor); } else { mPaint.setColor(mFingerUpUnmatchedColor); } for (ILockView iLockView : mILockViews) { if (mChooseList.contains(iLockView.getView().getId())) { if (!isMatched) { iLockView.onFingerUpUnmatched(); } else { iLockView.onFingerUpMatched(); } } } } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); //画Path canvas.drawPath(mPath, mPaint); //画指引线 if (mChooseList.size() > 0) { canvas.drawLine(mLastPathX, mLastPathY, mLineX, mLineY, mPaint); } } /** * 设置LockView * * @param lockViewFactory */ public void setLockView(LockViewFactory lockViewFactory) { if (lockViewFactory != null) { removeAllViewsInLayout(); mILockViews.clear(); mLockViewFactory = lockViewFactory; if (mLockViewWidth > 0) { setLockViewParams(mLockViewFactory); reset(); } } } /** * 设置答案 * * @param answer */ public void setAnswer(int... answer) { mAnswerList.clear(); for (int i = 0; i < answer.length; i++) { mAnswerList.add(answer[i]); } } /** * 将String类型的Answer设置到list * 必须时List的toString形式[x,x,x] * * @param answer */ public void setAnswer(String answer) { if (answer.startsWith("[") && answer.endsWith("]")) { answer = answer.substring(1, answer.length() - 1); String[] answers = answer.split(","); mAnswerList.clear(); for (int i = 0; i < answers.length; i++) { mAnswerList.add(Integer.parseInt(answers[i].trim())); } } } /** * 设置是否可以触摸 * * @param touchable */ public void setTouchable(boolean touchable) { this.mTouchable = touchable; reset(); invalidate(); } /** * 设置手势解锁监听器 * * @param listener */ public void setOnLockVerifyListener(OnLockVerifyListener listener) { this.mOnLockVerifyListener = listener; } public void setOnLockResetListener(OnLockResetListener listener) { this.mOnLockResetListener = listener; } /** * 设置路径宽度 * * @param dp */ public void setPathWidth(float dp) { mPaint.setStrokeWidth(HDLUtlisXM.dp2px(getContext(), dp)); } /** * 设置每行点的个数 * * @param count */ public void setDotCount(int count) { this.mDotCount = count; } /** * 设置手指按下时Path颜色 * * @param color */ public void setTouchedPathColor(int color) { this.mFingerTouchColor = color; DEFAULT_SELECT_COLOR = color; } /** * 设置正确时的颜色 * * @param color */ public void setTruePathColor(int color) { this.mFingerTouchColor = color; this.mFingerUpMatchedColor = color; DEFAULT_SELECT_COLOR = color; } /** * 设置手指抬起时,密码匹配颜色 * * @param color */ public void setMatchedPathColor(int color) { this.mFingerUpMatchedColor = color; DEFAULT_SELECT_COLOR = color; } /** * 设置手指抬起时,密码不匹配的颜色 * * @param color */ public void setUnmatchedPathColor(int color) { this.mFingerUpUnmatchedColor = color; DEFAULT_SELECT_FALSE_COLOR = color; } /** * 切换LockView是否匹配状态 * * @param isMatched */ public void setLockViewMatchedState(Boolean isMatched){ toggleLockViewMatchedState(isMatched); resetGestureTimer(); } /** * 设置重置延时时间 * * @param resetDelayMillis */ public void setResetDelayMillis(int resetDelayMillis) { this.resetDelayMillis = resetDelayMillis; } // /** // * 设置最大尝试次数 // * // * @param tryTimes // */ // public void setTryTimes(int tryTimes) { // this.mTryTimes = tryTimes; // this.mSavedTryTimes = tryTimes; // } // /** // * 获取最大尝试次数 // * // * @return // */ // public int getTryTimes() { // return mTryTimes; // } // /** // * 设置密码模式下,最小连接数 // * // * @param minCount // */ // public void setMinCount(int minCount) { // this.mMinCount = minCount; // } // public void setMode(int mode) { // this.mCurrentMode = mode; // reset(); // //切换到验证模式的时候,还原最大尝试次数 // if (mCurrentMode == VERIFY_MODE) { //// mTryTimes = mSavedTryTimes; // } else if (mCurrentMode == RESET_MODE) { // //清除已有密码数据 // mAnswerList.clear(); // } // } public interface OnLockVerifyListener { /** * 移动过程中选中的id * * @param id */ void onGestureSelected(int id); // /** // * 手势动作完成 // * // * @param isMatched 是否和密码匹配 // */ // void onGestureFinished(boolean isMatched); /** * 手势动作完成 * * @param selectPassword 选择的密码 */ void onGestureFinished(String selectPassword, int selectCount); // /** // * 超过尝试次数上限 // */ // void onGestureTryTimesBoundary(); } public interface OnLockResetListener { /** * 连接数不符 * * @param connectCount * @param minCount */ void onConnectCountUnmatched(int connectCount, int minCount); /** * 连接数符合,第一次密码设置成功 * * @param answerList */ void onFirstPasswordFinished(List answerList); /** * 设置密码成功 * * @param isMatched 两次密码是否匹配 * @param answerList 密码list */ void onSetPasswordFinished(boolean isMatched, List answerList); } /** * 保存状态bean */ static class SavedState extends BaseSavedState { int tryTimes; public SavedState(Parcelable source) { super(source); } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); tryTimes = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeValue(tryTimes); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }