1. 前言

  • 最近想给自己做的的app添加一个滑动解锁的功能,用的是乐视的手机,就模仿它的效果实现.

  • 视频演示一下效果

  • GitHub


2. LockPoint实体

  • 每个点是一个实体(LockPoint)用来存储这个点的所有信息,包括点的物理位置(x,y)和点的index位置(0-8)
class LockPoint {// 点的位置 0-8int index;// 点的x,y坐标float x, y;// 构造方法,初始化一个点LockPoint(int index, float x, float y) {this.index = index;this.x = x;this.y = y;}// 构造方法,从另一个点初始化LockPoint(LockPoint p) {this.x = p.x;this.y = p.y;this.index = p.index;}// 默认构造方法,初始化为一个空的点LockPoint() {this.x = -1;this.y = -1;this.index = -1;}// 判断该点是不是一个空的点boolean isEmpty() {return this.x == -1 && this.y == -1;}// 重新给位置赋值void init(float x, float y) {this.x = x;this.y = y;}// 设置为另一点的值void init(LockPoint p) {this.x = p.x;this.y = p.y;this.index = p.index;}// 判断一个位置是不是在该点触摸范围内,touchSensitiveRange为触摸有效半径boolean isTouchIn(float judgeX, float judgeY) {return judgeX < x + touchSensitiveRange &&judgeX > x - touchSensitiveRange &&judgeY < y + touchSensitiveRange &&judgeY > y - touchSensitiveRange;}// 重写equals和hashCode@Overridepublic boolean equals(Object o) {LockPoint p = (LockPoint) o;return p.x == x && p.y == y;}@Overridepublic int hashCode() {return 2;}String out(String tag) {return tag + " : x = " + x + " , y = " + y;}}

3. 初始化

  • 初始化九个点的位置,需要根据控件的大小动态计算,因此在onMeare()之后进行
  • 需求是需要将九个点放在控件中间,来适应控件大小的变化,首先确定第一个点距离左边的距离startSpace,两个点之间的距离 =(控件宽度 - 2 * startSpace)/2
        int size = getMeasuredWidth();// 将宽高设置为一样的,正方形setMeasuredDimension(size, size);// 初始化屏幕中的九个点的位置和下标initLockPointArray = new LockPoint[9];// startSpace 为距离左边的距离,计算九个点的位置,保证九个点放在控件中间if (startSpace == AUTO_START_SPACING) {//默认是控件的1/4startSpace = size / 4;}// 计算每两个点之间的间隔internalSpace = (size - 2 * startSpace) / 2;
  • 初始化九个点的位置
            // 初始化九个点的位置int index = 0;for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {initLockPointArray[index] = new LockPoint(index, startSpace + j * internalSpace, startSpace + i * internalSpace);index++;}}
  • onMeasure()完整代码
// onMeasure之后初始化数据@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int size = getMeasuredWidth();// 将宽高设置为一样的,正方形setMeasuredDimension(size, size);// 初始化屏幕中的九个点的位置和下标if (initLockPointArray == null) {initLockPointArray = new LockPoint[9];// startSpace 为距离左边的距离,计算九个点的位置放在控件中间if (startSpace == AUTO_START_SPACING) {startSpace = size / 4;}// 计算每两个点之间的间隔internalSpace = (size - 2 * startSpace) / 2;// 初始化九个点的位置int index = 0;for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {initLockPointArray[index] = new LockPoint(index, startSpace + j * internalSpace, startSpace + i * internalSpace);index++;}}// 为了在preview时能看到效果if (isInEditMode()) {historyPointList.addAll(Arrays.asList(initLockPointArray));}}}

4. onDraw

  • 绘制过程大致分为三个步骤

  • <1> 绘制九个点,这是每次都需要绘制的

        LockPoint tempPoint;for (int i = 0; i < initLockPointArray.length; i++) {canvas.drawCircle(initLockPointArray[i].x, initLockPointArray[i].y, pointRadius, pointPaint);}
  • <2> 绘制已经划过的点
        // 绘制之前触过存储起来的的点,绘制第i个点和i+1个点之间的线if (historyPointList.size() > 0) {for (int i = 0; i < historyPointList.size() - 1; i++) {canvas.drawLine(historyPointList.get(i).x, historyPointList.get(i).y, historyPointList.get(i + 1).x, historyPointList.get(i + 1).y, linePaint);}}
  • <3> 绘制触摸点和最后一个点的连线
        // 画最后一个点和触摸的点之间的线if (currentLockPoint != null&& currentLockPoint.x != -1 && currentLockPoint.y != -1&& touchPoint.x != -1 && touchPoint.y != -1) {canvas.drawLine(currentLockPoint.x, currentLockPoint.y, touchPoint.x, touchPoint.y, linePaint);}

5. 事件处理

  • 对用户touch事件进行处理

    1. 要记录当前触摸的点,用于绘制跟随手指的连线
    2. 检测触摸的点是不是在九个点中某个点的范围内,如果是的话该点要加入被触摸点的列表中
    3. 当手指抬起时,清除数据,恢复初始状态
    @Overridepublic boolean onTouchEvent(MotionEvent event) {if (!isEnabled() || isEventOver)return false;int action = MotionEventCompat.getActionMasked(event);switch (action) {// 重新初始化触摸点case MotionEvent.ACTION_DOWN:touchPoint.init(event.getX(), event.getY());break;// 移动时检测是否在触摸范围内case MotionEvent.ACTION_MOVE:touchPoint.init(event.getX(), event.getY());LockPoint tempPoint;for (int i = 0; i < initLockPointArray.length; i++) {tempPoint = initLockPointArray[i];if (!historyPointList.contains(tempPoint)&& tempPoint.isTouchIn(event.getX(), event.getY())) {historyPointList.add(new LockPoint(tempPoint));currentLockPoint.init(tempPoint);break;}}break;// 抬起时结束,重新初始化case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:touchPoint.init(-1, -1);currentLockPoint.init(-1, -1);historyPointList.clear();break;}postInvalidate();return true;}

6. 优化-多点触控事件处理

  • 用户在触摸屏幕时可能有多个手指在操作,上面的代码在单指时没有问题,兼容多点触控的思路是:

    1. 当用户触发down事件时,我们可以获取到一个pointerId,这个id唯一的标志了这个指头,后面发生的所有事件都使用用这个pointerId来获取,只处理这个指头的事件,避免事件的错乱。
    2. 当我们开始的时候标志的那个手指抬起来了怎么办呢,两个解决方法,第一个就是直接结束整个流程,相当于单指时手指抬起。第二个方法就是转移事件,当一个指头抬起时,从该事件中获取还没抬起的手指,更改标志的pointerId,事件就转移到了另一个手指上,我们关心就是新手指的触摸啦
    3. 关于对于事件进行处理的相关机制可以看Android事件机制,写的都是比较基本的东西,后面慢慢完善,不过理解获取多指的事件9⃣️绰绰有余啦
  • 话不多说,上代码,比较需要注意的地方我都标注在注释中,方便查找
    // 处理触摸事件,支持多点触摸@Overridepublic boolean onTouchEvent(MotionEvent event) {// fast stopif (!isEnabled() || isEventOver)return false;// pointerIndex 是事件的在event中的下标int pointerIndex;// 获取事件掩码int action = MotionEventCompat.getActionMasked(event);switch (action) {// 重新初始化触摸点case MotionEvent.ACTION_DOWN:// pointerId 记录当前激活的pointerIdactivePointerId = event.getPointerId(0);// 根据pointerId查找事件在event中的位置pointerIndex = event.findPointerIndex(activePointerId);// 根据位置获取到具体的事件的坐标,这里获得的坐标就是我们要记住的那个指头的坐标touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));break;case MotionEvent.ACTION_MOVE:// 手指移动时还是根据激活的pointerId获取下标index,来进行后续操作,避免事件错乱pointerIndex = event.findPointerIndex(activePointerId);// pointerIndex < 0表示手指的事件获取不到了,结束响应事件if (pointerIndex < 0) {Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");cancelLockDraw();return false;}// 根据移动的位置获取坐标,初始化touchPoint的值touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));LockPoint tempPoint;// 检索触摸点有没有在九个点中的某一个的触摸范围内for (int i = 0; i < initLockPointArray.length; i++) {tempPoint = initLockPointArray[i];if (!historyPointList.contains(tempPoint)&& tempPoint.isTouchIn(event.getX(pointerIndex), event.getY(pointerIndex))) {LockPoint centerPoint = findCenterPoint(tempPoint);// 优化,查找两个点之间的点,后面会有介绍if (!centerPoint.isEmpty()) {activePoint(centerPoint);}activePoint(tempPoint);break;}}break;case MotionEventCompat.ACTION_POINTER_UP:// 多指操作中 非 最后一个手指抬起时触发ACTION_POINTER_UP,此时要获取还在屏幕上的其他手指转移事件的对象onSecondaryPointerUp(event);break;case MotionEvent.ACTION_UP:// 最后的手指抬起触发 ACTION_UPpointerIndex = event.findPointerIndex(activePointerId);if (pointerIndex < 0) {Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id.");activePointerId = INVALID_POINTER;return false;}// 发布绘制的结果,可能是监听回调之类的publishResult();// 置为-1activePointerId = INVALID_POINTER;break;case MotionEvent.ACTION_CANCEL:// 类似upcancelLockDraw();activePointerId = INVALID_POINTER;break;}postInvalidate();return true;}
  • 转移焦点的方法,在各种控件的源代码中随处可见,我也是拷贝出来直接用的,逻辑不是很复杂
    /*** 当一个手机抬起时,转移焦点** @param ev 事件*/
private void onSecondaryPointerUp(MotionEvent ev) {final int pointerIndex = MotionEventCompat.getActionIndex(ev);final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);if (pointerId == activePointerId) {// This was our active pointer going up. Choose a new// active pointer and adjust accordingly.final int newPointerIndex = pointerIndex == 0 ? 1 : 0;activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);}}
  • 发布结果
    /*** 发布绘制结果*/private void publishResult() {if (listener != null) {isEventOver = true;StringBuilder sb = new StringBuilder();for (LockPoint lockPoint : historyPointList) {sb.append(lockPoint.index);}String passWd = sb.toString();boolean isFinish = listener.onFinish(LockView.this, passWd, passWd.length());if (isFinish) {// 输入合法touchPoint.init(currentLockPoint);} else {// 输入不合法cancelLockDraw();isEventOver = false;}} else {cancelLockDraw();}}
  • 回复初始状态,因为在多处调用了,贴一下
    /*** 结束绘制,恢复初始状态*/private void cancelLockDraw() {touchPoint.init(-1, -1);currentLockPoint.init(-1, -1);historyPointList.clear();postInvalidate();}

7. 优化-自动添加两点之间连线上的点

  • 当滑动时越过中间的点之间连接两端,自动查找和添加两点之间的点,手机上的滑动解锁也是这样的逻辑,不然会导致图形很繁琐,不美观而且不符合常见逻辑。也就是说如果当前激发的点和上一个激发的点之间有没有激发的点,那么自动给他激发。

  • 首先如果两个点是相邻的或者是对角线上相邻,那么中间一定不会有空下来的点,需要排除这个情况

/*** 检测相邻** @param p1 点1* @param p2 点2* @return p1和p2是否相邻,斜对角也算相邻*/private boolean isAdjacentPoint(LockPoint p1, LockPoint p2) {// internalSpace是初始化时两个点之间的距离,都是简单的计算和情况罗列if ((p1.x == p2.x && Math.abs(p1.y - p2.y) == internalSpace)|| (p1.y == p2.y && Math.abs(p1.x - p2.x) == internalSpace)|| (Math.abs(p1.x - p2.x) == internalSpace && Math.abs(p1.y - p2.y) == internalSpace)) {Log.e(TAG, "相邻点,不处理");return true;}return false;}
  • 然后如何判断一个点位于首尾两个激发点的中间,思路是当这个点在两个点的连线上时且不是首尾两个点就是中间的点。判断的根据是斜率是不是相等,就是初中的数学问题啦。
    /*** 判断c点是不是在p1-p2的直线上** @param p1 起始点* @param p2 终止点* @param c  判断的点* @return 是否在该线上*/private boolean isInLine(LockPoint p1, LockPoint p2, LockPoint c) {float k1 = (p1.x - p2.x) * 1f / (p1.y - p2.y);float k2 = (p1.x - c.x) * 1f / (p1.y - c.y);return k1 == k2;}
  • 最后整合一下,去掉不必要的判断,在touch事件中调用
    /*** 检测当前激活的点和上一个激活点之间的是否有没有激发的点** @param activePoint 当前被激发的点* @return 当前激活的点和上一个激活点之间的是否有没有激发的点,没有返回empty的{@link LockPoint#isEmpty()}*/private LockPoint findCenterPoint(LockPoint activePoint) {LockPoint rstPoint = new LockPoint();// 只有一个点不需要比较if (historyPointList.size() < 1) {return rstPoint;}LockPoint tempPoint;// 获取上个点LockPoint preActivePoint = historyPointList.get(historyPointList.size() - 1);// 两个点是不是相邻的,是相邻的是坚决不会中间有点被空出来的if (isAdjacentPoint(preActivePoint, activePoint))return rstPoint;for (int i = 0; i < initLockPointArray.length; i++) {tempPoint = initLockPointArray[i];// 没有被触摸过 && 不是首点 && 不是尾点if (!historyPointList.contains(tempPoint) && !preActivePoint.equals(tempPoint) && !activePoint.equals(tempPoint)) { // 在连线上if (isInLine(preActivePoint, activePoint, tempPoint)) {Log.e(TAG, "点在线上 " + tempPoint.out("temp") + " " + preActivePoint.out("pre") + " " + activePoint.out("active"));rstPoint.init(tempPoint);break;}}}return rstPoint;}
  • 在onTouchEvent中调用
        LockPoint centerPoint = findCenterPoint(tempPoint);// 优化,查找两个点之间的点if (!centerPoint.isEmpty()) {activePoint(centerPoint);}activePoint(tempPoint);

8. 优化-给被触摸的点添加动画

  • 当手指触摸到一个点时,添加一个缩放动画来反馈触摸操作

  • 思路时,当触摸到一个点时使用ValueAnimator开启动画,不断改变半径的值,在绘制时达到实现缩放的效果

    /*** 开始缩放动画*/private void startScaleAnimation() {if (mScaleAnimator == null) {mScaleAnimator = ValueAnimator.ofFloat(1f, scaleMax, 1f);mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float scale = (float) animation.getAnimatedValue();// 不断改变半径的值scalePointRadius = pointRadius * scale;postInvalidate();}});mScaleAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic void onAnimationEnd(Animator animation) {// 动画结束后初始化回标准半径的值scalePointRadius = pointRadius;}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic void onAnimationRepeat(Animator animation) {}});mScaleAnimator.setDuration(scaleAnimDuration);}if (mScaleAnimator.isRunning())mScaleAnimator.end();mScaleAnimator.start();}
  • 同时在onDraw()方法中对刚刚触摸的点要进行绘制,更改onDraw()方法中绘制九个点的部分,对刚刚触摸的点使用缩放后的半径绘制。
        // 绘制九个点,当动画在执行时被激活的点会被放大LockPoint tempPoint;for (int i = 0; i < initLockPointArray.length; i++) {tempPoint = initLockPointArray[i];// 最后触摸的点if (currentLockPoint != null && currentLockPoint.equals(tempPoint)) {canvas.drawCircle(tempPoint.x, tempPoint.y, scalePointRadius, pointPaint);} else {canvas.drawCircle(tempPoint.x, tempPoint.y, pointRadius, pointPaint);}}

9. 回调

  • 使用监听将结果回调给使用者,在ACTION_UP时发布结果
        public interface OnLockFinishListener {/*** * @param lockView 控件* @param passWd 密码* @param passWsLength 密码长度* @return 当返回true时,画面将会定格在绘制结束后的状态,比如当密码输入正确的时候* 返回false时,画面会重新初始化回初始状态,比如密码重新二次输入确认或者密码错误的时候*/boolean onFinish(LockView lockView, String passWd, int passWsLength);}/*** 发布绘制结果*/private void publishResult() {if (listener != null) {isEventOver = true;StringBuilder sb = new StringBuilder();for (LockPoint lockPoint : historyPointList) {sb.append(lockPoint.index);}String passWd = sb.toString();boolean isFinish = listener.onFinish(LockView.this, passWd, passWd.length());if (isFinish) {// 画面定格touchPoint.init(currentLockPoint);} else {// 恢复初始化cancelLockDraw();isEventOver = false;}} else {cancelLockDraw();}}

10. 综上

  • 还遗留了一个点,就是自动添加中间的点时应该也是有动画效果的,暂时还没做,有空补上吧,希望大家指正。

11. 完整代码

/*** Project  : CdLibsTest* Package  : com.march.cdlibstest.widget* CreateAt : 2016/11/26* Describe : 自定义控件实现九宫格滑动解锁** @author chendong*/
public class LockView extends View {public static final String TAG = "LOCK_VIEW";private static final int INVALID_POINTER = -1;private static final int AUTO_START_SPACING = -1;private static final int DEFAULT_MIN_POINT_NUM = 4;// 激活的触摸点idprivate int activePointerId = INVALID_POINTER;// 四边的间隔,默认是控件的1/4private int startSpace;// 两点间隔private int internalSpace;// 点的半径private int pointRadius;// 动画scale的半径private float scalePointRadius;// 触摸半径,在点的一定范围内触发private int touchSensitiveRange;// 线宽度private int lineWidth;// 点颜色private int pointColor;// 线颜色private int lineColor;// 缩放的大小private float scaleMax;// 动画时间private int scaleAnimDuration = 150;// 本次绘制结束,调用init()方法恢复初始化private boolean isEventOver = false;class LockPoint {// 点的位置 0-8int index;//  点的x,y坐标float x, y;// 构造方法,初始化一个点LockPoint(int index, float x, float y) {this.index = index;this.x = x;this.y = y;}// 构造方法,从另一个点初始化LockPoint(LockPoint p) {this.x = p.x;this.y = p.y;this.index = p.index;}// 默认构造方法,初始化为一个空的点LockPoint() {this.x = -1;this.y = -1;this.index = -1;}// 判断该点是不是一个空的点boolean isEmpty() {return this.x == -1 && this.y == -1;}// 重新给位置赋值void init(float x, float y) {this.x = x;this.y = y;}// 设置为另一点的值void init(LockPoint p) {this.x = p.x;this.y = p.y;this.index = p.index;}// 判断一个位置是不是在该点触摸范围内,touchSensitiveRange为触摸有效半径boolean isTouchIn(float judgeX, float judgeY) {return judgeX < x + touchSensitiveRange &&judgeX > x - touchSensitiveRange &&judgeY < y + touchSensitiveRange &&judgeY > y - touchSensitiveRange;}// 重写equals和hashCode@Overridepublic boolean equals(Object o) {LockPoint p = (LockPoint) o;return p.x == x && p.y == y;}@Overridepublic int hashCode() {return 2;}String out(String tag) {return tag + " : x = " + x + " , y = " + y;}}// 动画private ValueAnimator mScaleAnimator;// 初始化的九个点private LockPoint[] initLockPointArray;// 触摸过的点泪飙private List<LockPoint> historyPointList;// 触摸的点private LockPoint touchPoint;// 当前最后一个激活的点private LockPoint currentLockPoint;// 画线private Paint linePaint;// 画点private Paint pointPaint;// 监听private OnLockFinishListener listener;public interface OnLockFinishListener {/**** @param lockView 控件* @param passWd 密码* @param passWsLength 密码长度* @return 当返回true时,画面将会定格在绘制结束后的状态* 返回false时,画面会重新初始化回初始状态*/boolean onFinish(LockView lockView, String passWd, int passWsLength);}public LockView(Context context) {this(context, null);}public LockView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public LockView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LockView);float density = getResources().getDisplayMetrics().density;pointRadius = (int) typedArray.getDimension(R.styleable.LockView_lock_pointRadius, (8 * density));scalePointRadius = pointRadius;touchSensitiveRange = (int) typedArray.getDimension(R.styleable.LockView_lock_touchSensitiveRange, pointRadius * 3);startSpace = (int) typedArray.getDimension(R.styleable.LockView_lock_startSpace, AUTO_START_SPACING);lineWidth = (int) typedArray.getDimension(R.styleable.LockView_lock_lineWidth, (5 * density));lineColor = typedArray.getColor(R.styleable.LockView_lock_lineColor, Color.WHITE);pointColor = typedArray.getColor(R.styleable.LockView_lock_pointColor, Color.WHITE);scaleAnimDuration = typedArray.getInt(R.styleable.LockView_lock_scaleAnimDuration, 180);scaleMax = typedArray.getFloat(R.styleable.LockView_lock_scaleMax, 2.5f);typedArray.recycle();historyPointList = new ArrayList<>();touchPoint = new LockPoint();currentLockPoint = new LockPoint();pointPaint = new Paint();pointPaint.setAntiAlias(true);pointPaint.setColor(pointColor);pointPaint.setStyle(Paint.Style.FILL_AND_STROKE);linePaint = new Paint();linePaint.setAntiAlias(true);linePaint.setStrokeWidth(lineWidth);linePaint.setColor(lineColor);linePaint.setStyle(Paint.Style.STROKE);}public void setListener(OnLockFinishListener listener) {this.listener = listener;}/*** 开始缩放动画*/private void startScaleAnimation() {if (mScaleAnimator == null) {mScaleAnimator = ValueAnimator.ofFloat(1f, scaleMax, 1f);mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float scale = (float) animation.getAnimatedValue();scalePointRadius = pointRadius * scale;postInvalidate();}});mScaleAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic void onAnimationEnd(Animator animation) {scalePointRadius = pointRadius;}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic void onAnimationRepeat(Animator animation) {}});mScaleAnimator.setDuration(scaleAnimDuration);}if (mScaleAnimator.isRunning())mScaleAnimator.end();mScaleAnimator.start();}// 处理触摸事件,支持多点触摸@Overridepublic boolean onTouchEvent(MotionEvent event) {// fast stopif (!isEnabled() || isEventOver)return false;// pointerIndex 是事件的在event中的下标int pointerIndex;// 获取事件掩码int action = MotionEventCompat.getActionMasked(event);switch (action) {// 重新初始化触摸点case MotionEvent.ACTION_DOWN:// pointerId 记录当前激活的pointerIdactivePointerId = event.getPointerId(0);// 根据pointerId查找事件在event中的位置pointerIndex = event.findPointerIndex(activePointerId);// 根据位置获取到具体的事件的坐标,这里获得的坐标就是我们要记住的那个指头的坐标touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));break;case MotionEvent.ACTION_MOVE:// 手指移动时还是根据激活的pointerId获取下标index,来进行后续操作,避免事件错乱pointerIndex = event.findPointerIndex(activePointerId);// pointerIndex < 0表示手指的事件获取不到了,结束响应事件if (pointerIndex < 0) {Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");cancelLockDraw();return false;}// 根据移动的位置获取坐标,初始化touchPoint的值touchPoint.init(event.getX(pointerIndex), event.getY(pointerIndex));LockPoint tempPoint;// 检索触摸点有没有在九个点中的某一个的触摸范围内for (int i = 0; i < initLockPointArray.length; i++) {tempPoint = initLockPointArray[i];if (!historyPointList.contains(tempPoint)&& tempPoint.isTouchIn(event.getX(pointerIndex), event.getY(pointerIndex))) {LockPoint centerPoint = findCenterPoint(tempPoint);if (!centerPoint.isEmpty()) {activePoint(centerPoint);}activePoint(tempPoint);break;}}break;case MotionEventCompat.ACTION_POINTER_UP:// 多指操作中 非 最后一个手指抬起时触发ACTION_POINTER_UP,此时要获取还在屏幕上的其他手指转移事件的对象onSecondaryPointerUp(event);break;case MotionEvent.ACTION_UP:// 最后的手指抬起触发 ACTION_UPpointerIndex = event.findPointerIndex(activePointerId);if (pointerIndex < 0) {Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id.");activePointerId = INVALID_POINTER;return false;}// 发布绘制的结果,可能是监听回调之类的publishResult();// 置为-1activePointerId = INVALID_POINTER;break;case MotionEvent.ACTION_CANCEL:// 类似upcancelLockDraw();activePointerId = INVALID_POINTER;break;}postInvalidate();return true;}/*** 发布绘制结果*/private void publishResult() {if (listener != null) {isEventOver = true;StringBuilder sb = new StringBuilder();for (LockPoint lockPoint : historyPointList) {sb.append(lockPoint.index);}String passWd = sb.toString();boolean isFinish = listener.onFinish(LockView.this, passWd, passWd.length());if (isFinish) {// 输入合法touchPoint.init(currentLockPoint);} else {// 输入不合法cancelLockDraw();isEventOver = false;}} else {cancelLockDraw();}}/*** 当一个手机抬起时,转移焦点** @param ev 事件*/private void onSecondaryPointerUp(MotionEvent ev) {final int pointerIndex = MotionEventCompat.getActionIndex(ev);final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);if (pointerId == activePointerId) {// This was our active pointer going up. Choose a new// active pointer and adjust accordingly.final int newPointerIndex = pointerIndex == 0 ? 1 : 0;activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);}}/*** 检测当前激活的点和上一个激活点之间的是否有没有激发的点** @param activePoint 当前被激发的点* @return 当前激活的点和上一个激活点之间的是否有没有激发的点,没有返回empty的{@link LockPoint#isEmpty()}*/private LockPoint findCenterPoint(LockPoint activePoint) {LockPoint rstPoint = new LockPoint();// 只有一个点不需要比较if (historyPointList.size() < 1) {return rstPoint;}LockPoint tempPoint;// 获取上个点LockPoint preActivePoint = historyPointList.get(historyPointList.size() - 1);// 两个点是不是相邻的,是相邻的是坚决不会中间有点被空出来的if (isAdjacentPoint(preActivePoint, activePoint))return rstPoint;for (int i = 0; i < initLockPointArray.length; i++) {tempPoint = initLockPointArray[i];// 没有被触摸过 && 不是首点 && 不是尾点if (!historyPointList.contains(tempPoint) && !preActivePoint.equals(tempPoint) && !activePoint.equals(tempPoint)) {if (isInLine(preActivePoint, activePoint, tempPoint)) {Log.e(TAG, "点在线上 " + tempPoint.out("temp") + " " + preActivePoint.out("pre") + " " + activePoint.out("active"));rstPoint.init(tempPoint);break;}}}return rstPoint;}/*** 检测相邻** @param p1 点1* @param p2 点2* @return p1和p2是否相邻,斜对角也算相邻*/private boolean isAdjacentPoint(LockPoint p1, LockPoint p2) {if ((p1.x == p2.x && Math.abs(p1.y - p2.y) == internalSpace)|| (p1.y == p2.y && Math.abs(p1.x - p2.x) == internalSpace)|| (Math.abs(p1.x - p2.x) == internalSpace && Math.abs(p1.y - p2.y) == internalSpace)) {Log.e(TAG, "相邻点,不处理");return true;}return false;}/*** 判断c点是不是在p1-p2的直线上** @param p1 起始点* @param p2 终止点* @param c  判断的点* @return 是否在该线上*/private boolean isInLine(LockPoint p1, LockPoint p2, LockPoint c) {float k1 = (p1.x - p2.x) * 1f / (p1.y - p2.y);float k2 = (p1.x - c.x) * 1f / (p1.y - c.y);return k1 == k2;}/*** 激活该点,该点将会添加到选中点列表中,然后执行动画** @param tempPoint 被激活的点*/private void activePoint(LockPoint tempPoint) {historyPointList.add(new LockPoint(tempPoint));currentLockPoint.init(tempPoint);startScaleAnimation();postInvalidate();}public void init() {isEventOver = false;cancelLockDraw();}/*** 结束绘制,恢复初始状态*/private void cancelLockDraw() {touchPoint.init(-1, -1);currentLockPoint.init(-1, -1);historyPointList.clear();postInvalidate();}// onMeasure之后初始化数据@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int size = getMeasuredWidth();// 将宽高设置为一样的,正方形setMeasuredDimension(size, size);// 初始化屏幕中的九个点的位置和下标if (initLockPointArray == null) {initLockPointArray = new LockPoint[9];// startSpace 为距离左边的距离,计算九个点的位置放在控件中间if (startSpace == AUTO_START_SPACING) {startSpace = size / 4;}// 计算每两个点之间的间隔internalSpace = (size - 2 * startSpace) / 2;// 初始化九个点的位置int index = 0;for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {initLockPointArray[index] = new LockPoint(index, startSpace + j * internalSpace, startSpace + i * internalSpace);index++;}}// 为了在preview时能看到效果if (isInEditMode()) {historyPointList.addAll(Arrays.asList(initLockPointArray));}}}private void log(Object... objs) {StringBuilder sb = new StringBuilder();for (Object obj : objs) {sb.append(obj.toString()).append("   ");}Log.e(TAG, sb.toString());}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);// fast stopif (initLockPointArray == null)return;log(currentLockPoint.out("current"), touchPoint.out("touch"));// 画最后一个点和触摸的点之间的线if (currentLockPoint != null&& currentLockPoint.x != -1 && currentLockPoint.y != -1&& touchPoint.x != -1 && touchPoint.y != -1) {canvas.drawLine(currentLockPoint.x, currentLockPoint.y, touchPoint.x, touchPoint.y, linePaint);}// 绘制之前触过存储起来的的点if (historyPointList.size() > 0) {for (int i = 0; i < historyPointList.size() - 1; i++) {canvas.drawLine(historyPointList.get(i).x, historyPointList.get(i).y, historyPointList.get(i + 1).x, historyPointList.get(i + 1).y, linePaint);}}// 绘制九个点,当动画在执行时被激活的点会被放大LockPoint tempPoint;for (int i = 0; i < initLockPointArray.length; i++) {tempPoint = initLockPointArray[i];if (currentLockPoint != null && currentLockPoint.equals(tempPoint)) {canvas.drawCircle(tempPoint.x, tempPoint.y, scalePointRadius, pointPaint);} else {canvas.drawCircle(tempPoint.x, tempPoint.y, pointRadius, pointPaint);}}}
}

自定义控件九宫格滑动解锁相关推荐

  1. Appium九宫格滑动解锁研究

    九宫格滑动解锁,目前发现有两种一种是每个可点的方格都是一个imageview,九宫格则对应有9个imageview,此类解锁已有前人研究解决了,可以参照tobecrazy的博客 今天我要说的是另一种, ...

  2. android自定义滑块解锁,使用Android自定义控件实现滑动解锁九宫格

    本文概述: 滑动解锁九宫格的分析: 1.需要自定义控件: 2.需要重写事件onTouchEvent(); 3.需要给九个点设置序号和坐标,这里用Map类就行: 4.需要判断是否到滑到过九点之一,并存储 ...

  3. python自动化滑动解锁_python九宫格滑动解锁

    实现思路: 1.获取九宫格patterview的起始坐标x.y 2.获取九宫格patterview的宽度(width).高度(height) 3.九宫格的九个格子大约平均把patterview的长.宽 ...

  4. 手机九宫格滑动解锁方法种数(389112种)

    有妹子问手机滑动解锁多少种方案,于是写了个记忆化搜索得出答案,也是有趣. #include <iostream> #include <algorithm> #include & ...

  5. 自定义view实战(11):滑动解锁九宫格控件

    前言 上一篇文章用贝塞尔曲线画了一个看起来不错的小红点功能,技术上没什么难度,主要就是数学上的计算.这篇文章也差不多,模仿了一个常用的滑动解锁的九宫格控件. 需求 用过安卓的都知道,用过苹果的也知道, ...

  6. java实现九宫格解锁_轻松实现Android自定义九宫格图案解锁

    Android实现九宫格图案解锁,自带将图案转化成数字密码的功能,代码如下: LockPatternView.java package com.jackie.lockpattern; import a ...

  7. Android自定义九宫格手势解锁组件

    项目来源 别的不说,先看一下效果图: 九宫格手势图案解锁功能在很多应用中都在使用,本文介绍的组件来自于开源项目PatternLocker的翻写,原工程是使用Kotlin开发的,由于我们项目是使用jav ...

  8. Android进阶之自定义View实战(二)九宫格手势解锁实现

    一.引言 在上篇博客Android进阶之自定义View实战(一)仿iOS UISwitch控件实现中我们主要介绍了自定义View的最基本的实现方法.作为自定义View的入门篇,仅仅介绍了Canvas的 ...

  9. android ios滑动解锁效果,Android 高仿 IOS7 IPhone 解锁 Slide To Unlock 附源码

    0. 源码下载 1. IPhone 解锁 效果图: 在最新的IOS7中,苹果更改了解锁方式,整个屏幕向右滑动都可以解锁,不再局限在一个小的矩形中.这种文字加亮移动的效果还是继承了下来.之前滑动最左边的 ...

最新文章

  1. 重新编号_武汉黄陂公交线路PW、PG、PZ……分不清?别急,就要重新编号啦
  2. centos6.2下配置nfs
  3. MoeCTF 2021Re部分------time2go
  4. ITK:使用Otsu方法将前景和背景分开
  5. 2、C#基础 - Visual Studio 的版本选择和下载
  6. 谷歌退出中国几成定局 谈判已谈崩
  7. python 计算每日累计_一颗韭菜的自我修养:用 Python 分析下股市,练练手
  8. pycharm写python代码_使用pycharm写python代码的一些提高效率的技巧(持续更新)-Go语言中文社区...
  9. 跟着图灵去听课——海底捞敏捷之道纪要
  10. ***引发《唐山大地震》 在线下载瞬间中毒
  11. linux mysql5.7 实例初始化_mysql 5.7多实例单配置文件安装
  12. eclipse做html登录界面代码,在eclipse中怎麽编写一个登陆界面的代码,如新浪邮箱的登陆的? 爱问知识人...
  13. 注册测绘师学习笔记(二)
  14. 随机出题在线考试系统php_随机出题在线考试系统
  15. 数据仓库-事实表和维度表的设计
  16. 于飞SEO:零基础学seo难吗?怎么学?
  17. 软件推荐:强力卸载软件HIBIT
  18. 支付宝通过招行网上银行付钱,最多每笔500块
  19. JVM堆内存(heap)
  20. 新编计算机英语复习(东华理工专业外语)

热门文章

  1. linux 计价软件,asterCC
  2. docker安装手册
  3. 打车费用计算,输入公里数,得到费用
  4. linux更mysql改密码_Linux下mysql密码的两种修改方式
  5. MYSQL中UNION(联合查询)
  6. 孩子学python好还是c加加好 能够锻炼孩子思维能力_网上说编程可以锻炼孩子思维能力什么的,是真的吗?...
  7. 使用solidworks2018制作一个简单的装配体
  8. pip换源教程(windows)
  9. OpenGL4.0学习3.1--初始化OpenGL 4.0
  10. mysql和xampp的区别_phpstudy与xampp区别