Android手势密码原理分析

在上一篇介绍了手势密码的使用,这一篇将主要介绍手势密码的原理,手势密码的功能主要是由自定义PatternLockView实现的。那咱这就一步一步来揭开PatternLockView的面纱。

效果图

这里写图片描述这里写图片描述这里写图片描述

步骤

第一步

自定义PatternLockView继承View,重写两个构造方法,一个在xml中定义会调用,一个在java代码中创建对象会调用。但不管怎么定义,都会走到这个构造中。

public PatternLockView(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PatternLockView);
        try {
            sDotCount = typedArray.getInt(R.styleable.PatternLockView_dotCount,
                    DEFAULT_PATTERN_DOT_COUNT);
            mPathWidth = (int) typedArray.getDimension(R.styleable.PatternLockView_pathWidth,
                    ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_path_width));
            mNormalDotStateColor = typedArray.getColor(R.styleable.PatternLockView_normalDotStateColor,
                    ResourceUtils.getColor(getContext(), R.color.white));
            mCorrectDotStateColor = typedArray.getColor(R.styleable.PatternLockView_correctDotStateColor,
                    ResourceUtils.getColor(getContext(), R.color.white));
            mCorrectDotStrokeColor = typedArray.getColor(R.styleable.PatternLockView_correctDotStrokeStateColor,
                    ResourceUtils.getColor(getContext(), R.color.white));
            mCorrectLineStateColor = typedArray.getColor(R.styleable.PatternLockView_correctLineStateColor,
                    ResourceUtils.getColor(getContext(), R.color.white));
            mWrongLineStateColor = typedArray.getColor(R.styleable.PatternLockView_wrongLineStateColor,
                    ResourceUtils.getColor(getContext(), R.color.white));
            mWrongDotStateColor = typedArray.getColor(R.styleable.PatternLockView_wrongDotStateColor,
                    ResourceUtils.getColor(getContext(), R.color.white));
            mWrongDotStrokeStateColor = typedArray.getColor(R.styleable.PatternLockView_wrongDotStrokeStateColor,
                    ResourceUtils.getColor(getContext(), R.color.white));
            mDotNormalSize = (int) typedArray.getDimension(R.styleable.PatternLockView_dotNormalSize,
                    ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_dot_size));
            mDotSelectedSize = (int) typedArray.getDimension(R.styleable.PatternLockView_dotSelectedSize,
                    ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_dot_selected_size));
            mDotAnimationDuration = typedArray.getInt(R.styleable.PatternLockView_dotAnimationDuration,
                    DEFAULT_DOT_ANIMATION_DURATION);
        } finally {
            typedArray.recycle();
        }
        //获取绘制点的个数
        mPatternSize = sDotCount * sDotCount;
        //存放选中的点
        mPattern = new ArrayList<>(mPatternSize);
        //二维数组 选中点时置为true
        mPatternDrawLookup = new boolean[sDotCount][sDotCount];
        //二维数组 存放点对象
        mDotStates = new DotState[sDotCount][sDotCount];
        //通过循环的方式,创建点对象,并对点初始化大小
        for (int i = 0; i < sDotCount; i++) {
            for (int j = 0; j < sDotCount; j++) {
                mDotStates[i][j] = new DotState();
                mDotStates[i][j].mSize = mDotNormalSize;
            }
        }
        //存放监听对象
        mPatternListeners = new ArrayList<>();
        initView();
    }

这里主要拿到我们自定义的点的数量 点的颜色 线的颜色 线的宽度等信息,创建了存放选中点的集合 存放监听对象的集合和两个二维数组及点对象。

再来看下initView()方法

private void initView() {
        //设置View可点击
        setClickable(true);

        //设置点与点连线画笔
        mPathPaint = new Paint();
        mPathPaint.setAntiAlias(true);
        mPathPaint.setDither(true);
        mPathPaint.setColor(mCorrectLineStateColor);
        mPathPaint.setStyle(Paint.Style.STROKE);
        //当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式Cap.ROUND,或方形样式Cap.SQUARE
        mPathPaint.setStrokeJoin(Paint.Join.ROUND);
        //设置绘制时各图形的结合方式,如平滑效果等
        mPathPaint.setStrokeCap(Paint.Cap.ROUND);
        mPathPaint.setStrokeWidth(mPathWidth);

        //设置画点的画笔
        mDotPaint = new Paint();
        mDotPaint.setAntiAlias(true);
        mDotPaint.setDither(true);

        //设置点外环的画笔
        mRingPaint = new Paint();
        mRingPaint.setAntiAlias(true);
        mRingPaint.setDither(true);

        //设置点放大缩小动画(用于隐藏模式)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !isInEditMode()) {
            mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
                    getContext(), android.R.interpolator.fast_out_slow_in);
            mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
                    getContext(), android.R.interpolator.linear_out_slow_in);
        }
    }

这里主要对各个画笔做了初始化工作,也可以通过Java代码进行更改,初始化工作完成后,就要捕捉手势了。

第二步

通过onTouchEvent()方法获取手势坐标

@Override
public boolean onTouchEvent(MotionEvent event) {
        //判断View是否可用
        if (!mInputEnabled || !isEnabled()) {
            return false;
        }

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                handleActionDown(event);
                return true;
            case MotionEvent.ACTION_UP:
                handleActionUp(event);
                return true;
            case MotionEvent.ACTION_MOVE:
                handleActionMove(event);
                return true;
            case MotionEvent.ACTION_CANCEL:
                mPatternInProgress = false;
                resetPattern();
                notifyPatternCleared();
                return true;
        }
        return false;
    }

这里对手指的按下 移动 抬起 取消都做了判断
取消的操作比较简单,就是重置各个状态

下边分别来看下按下 移动 抬起

handleActionDown按下方法

private void handleActionDown(MotionEvent event) {
        //每次按下都重置下状态
        resetPattern();
        //获取手指按下的坐标,以View区域的左上顶点为(0,0)坐标
        float x = event.getX();
        float y = event.getY();
        //通过detectAndAddHit方法判断首次按下是否触碰到点
        Dot hitDot = detectAndAddHit(x, y);

        //如果触碰到点,则改变状态,开始画选中点颜色和与点的连线
        //如果没有触碰上,则不画线(通过mPatternInProgress状态控制)
        if (hitDot != null) {
            mPatternInProgress = true;
            mPatternViewMode = CORRECT;
            notifyPatternStarted();
        } else {
            mPatternInProgress = false;
            notifyPatternCleared();
        }
        //如果首次触碰到点,则进行局部刷新
        if (hitDot != null) {
            //获取选中点的中心坐标
            float startX = getCenterXForColumn(hitDot.mColumn);
            float startY = getCenterYForRow(hitDot.mRow);

            float widthOffset = mViewWidth / 2f;
            float heightOffset = mViewHeight / 2f;

            // 局部刷新其实没多大用,在onDraw里边还是所有代码都走
            invalidate((int) (startX - widthOffset),
                    (int) (startY - heightOffset),
                    (int) (startX + widthOffset), (int) (startY + heightOffset));
        }
        //把首次按下的坐标赋值给mInProgressX mInProgressY
        //当选中时可开始画线
        mInProgressX = x;
        mInProgressY = y;
    }

按下操作主要是获取首次按下坐标,并通过坐标拿到是否触碰到某一个点上,如果触碰到点上,则改变点的颜色并开始画点与手指的连线。

handleActionMove移动方法

private void handleActionMove(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        Dot hitDot = detectAndAddHit(x, y);
        int patternSize = mPattern.size();
        //手指按下时没有选中点,当滑动时,滑动到点上进入此判断语句
        //改变mPatternInProgress mPatternViewMode状态,重画选中点和外环的颜色及开始画与点的连线
        if (hitDot != null && patternSize == 1) {
            mPatternInProgress = true;
            mPatternViewMode = CORRECT;
            notifyPatternStarted();
        }

        mInProgressX = event.getX();
        mInProgressY = event.getY();
        //简单粗暴 重新绘制
        invalidate();
    }

移动操作主要是根据不断移动的坐标,判断是否触碰到点上,如果触碰上了就添加到选中点的集合内,并重画选中点和外环的颜色及开始画与点的连线。

handleActionUp抬起方法

private void handleActionUp(MotionEvent event) {
        // 判断手势集合中是否有选中的点
        if (!mPattern.isEmpty()) {
            //重置状态 阻止画最后一个点与手指之间的连线
            mPatternInProgress = false;
            notifyPatternDetected();
            //抬起的时候再绘制一次
            invalidate();
        }
    }

抬起操作主要是判断存放点的集合是否为空,如果不为空,则重置状态,阻止画最后一个点与手指之间的连线。

detectAndAddHit方法

/**
* 根据x,y坐标确定是否点击到点上
*/
private Dot detectAndAddHit(float x, float y) {
        //通过checkForNewHit判断坐标是否在一个点上
        final Dot dot = checkForNewHit(x, y);
        //如果在则返回点 如果不在则返回null
        if (dot != null) {
            Dot fillInGapDot = null;
            //拿到初始化定义存放选中点的集合
            final ArrayList<Dot> pattern = mPattern;
            if (!pattern.isEmpty()) {
                //拿到集合中最后一个点
                Dot lastDot = pattern.get(pattern.size() - 1);
                int dRow = dot.mRow - lastDot.mRow;
                int dColumn = dot.mColumn - lastDot.mColumn;

                int fillInRow = lastDot.mRow;
                int fillInColumn = lastDot.mColumn;

                //重新计算行
                /**
                 * 重新计算行
                 * 例如:从(0,1)直接到(2,1),想跳过(1,1)时,则通过此方法把第一行添加进去
                 */
                if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
                    fillInRow = lastDot.mRow + ((dRow > 0) ? 1 : -1);
                }
                //重新计算列
                if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
                    fillInColumn = lastDot.mColumn + ((dColumn > 0) ? 1 : -1);
                }
                //
                fillInGapDot = Dot.of(fillInRow, fillInColumn);
            }

            /**
             * 例如:从(0,1)直接到(2,1),想跳过(1,1)时,则通过此方法把第一行添加进去
             */
            if (fillInGapDot != null
                    && !mPatternDrawLookup[fillInGapDot.mRow][fillInGapDot.mColumn]) {
                addCellToPattern(fillInGapDot);
            }
            /**
             * 如果中间跳过一个点则先添加跳过的点
             * 如果没有则添加选中的点
             */
            addCellToPattern(dot);
            /**
             * 判断是否震动
             */
            if (mEnableHapticFeedback) {
                performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
                                | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
            }
            return dot;
        }
        return null;
    }

detectAndAddHit方法主要是根据坐标拿到点,并判断前一个点与现在点中间是否还有必须经过但还有添加的点,如果有则优先添加中间点,如果没有则直接添加现在的点。

checkForNewHit方法

/**
     * 检查是否滑动到点上
     */
    private Dot checkForNewHit(float x, float y) {
        //判断y坐标是否在点上
        final int rowHit = getRowHit(y);
        if (rowHit < 0) {
            return null;
        }
        //判断x坐标是否在点上
        final int columnHit = getColumnHit(x);
        if (columnHit < 0) {
            return null;
        }

        /**
         * 如果已经选中,则不再选中
         */
        if (mPatternDrawLookup[rowHit][columnHit]) {
            return null;
        }
        //返回一个点对象
        return Dot.of(rowHit, columnHit);
    }

checkForNewHit方法主要是根据x,y坐标判断是否在一个点上,如果是则返回一个点对象。
getRowHit getColumnHit方法

    /**
     * 根据y坐标判断是否在某一个点的y坐标上
     * if (y >= hitTop && y <= hitTop + hitSize)这行给点加了一个范围,在此范围内都算点上了点
     */
    private int getRowHit(float y) {
        final float squareHeight = mViewHeight;
        float hitSize = squareHeight * mHitFactor;

        float offset = getPaddingTop() + (squareHeight - hitSize) / 2f;
        for (int i = 0; i < sDotCount; i++) {
            float hitTop = offset + squareHeight * i;
            if (y >= hitTop && y <= hitTop + hitSize) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 根据x坐标判断是否在某一个点的x坐标上
     * if (x >= hitLeft && x <= hitLeft + hitSize)这行给点加了一个范围,在此范围内都算点上了点
     */
    private int getColumnHit(float x) {
        final float squareWidth = mViewWidth;
        float hitSize = squareWidth * mHitFactor;

        float offset = getPaddingLeft() + (squareWidth - hitSize) / 2f;
        for (int i = 0; i < sDotCount; i++) {

            final float hitLeft = offset + squareWidth * i;
            if (x >= hitLeft && x <= hitLeft + hitSize) {
                return i;
            }
        }
        return -1;
    }

这两个方法是给点加了一个范围,在此范围内都算点上了点。

addCellToPattern方法

private void addCellToPattern(Dot newDot) {
        //将选中的点置为true
        mPatternDrawLookup[newDot.mRow][newDot.mColumn] = true;
        //给集合中添加选中的点
        mPattern.add(newDot);
        //开始点放大缩小动画(隐藏模式用)
        startDotSelectedAnimation(newDot);
        //回调给用户选中的点
        notifyPatternProgress();
    }

addCellToPattern方法是将选中的点添到集合中,并把mPatternDrawLookup中点的位置改为true。

以上就是按下 移动 抬起的主要方法,下面再来看下点和线是如何绘制的。

第三步

首先通过onSizeChanged方法来确定patternlockview的大小

onSizeChanged方法

    @Override
    protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
        //设置每一个点占据的宽度大小
        int adjustedWidth = width - getPaddingLeft() - getPaddingRight();
        mViewWidth = adjustedWidth / (float) sDotCount;
        //设置每一个点占据的高度度大小
        int adjustedHeight = height - getPaddingTop() - getPaddingBottom();
        mViewHeight = adjustedHeight / (float) sDotCount;
    }

通过onSizeChanged方法,获取到一个点占据的宽和高
然后就是onDraw绘制方法了,所有的绘制工作都是由它来完成,同时在滑动过程中,此方法也是在不断的调用。

onDraw方法

    @Override
    protected void onDraw(Canvas canvas) {
        //拿到选中点的集合
        ArrayList<Dot> pattern = mPattern;
        int patternSize = pattern.size();
        //拿到存放点是否选中的二维数组
        boolean[][] drawLookupTable = mPatternDrawLookup;
        //拿到路径对象
        Path currentPath = mCurrentPath;
        //清除所有的直线和曲线的路径,但保持内部数据结构,以便更快地重用。
        currentPath.rewind();

        //判断是否是隐藏模式
        boolean drawPath = !mInStealthMode;
        if (drawPath) {
            //根据当前模式设置连接线的颜色
            mPathPaint.setColor(getCurrentLineStateColor());
            //是否绘制最后一个点与手指之间的连线
            boolean anyCircles = false;
            float lastX = 0f;
            float lastY = 0f;
            for (int i = 0; i < patternSize; i++) {
                //拿到选中的点
                Dot dot = pattern.get(i);

                //如果点没有被选中,则不需要跳出循环
                if (!drawLookupTable[dot.mRow][dot.mColumn]) {
                    return;
                }
                anyCircles = true;
                //拿到某点的中心坐标
                float centerX = getCenterXForColumn(dot.mColumn);
                float centerY = getCenterYForRow(dot.mRow);
                if (i != 0) {
                    DotState state = mDotStates[dot.mRow][dot.mColumn];
                    currentPath.rewind();
                    //当到第二个的时候,则lastX,lastY就是选中的第一个点的中心
                    currentPath.moveTo(lastX, lastY);
                    if (state.mLineEndX != Float.MIN_VALUE
                            && state.mLineEndY != Float.MIN_VALUE) {//主要用于自动绘制
                        currentPath.lineTo(state.mLineEndX, state.mLineEndY);
                    } else {//手动绘制时进入的
                        currentPath.lineTo(centerX, centerY);
                    }
                    //把线连接上
                    canvas.drawPath(currentPath, mPathPaint);
                }
                //当是第一个时,先拿到第一个的中心点,然后从第一个开始画线,后续再把下一个点的中心点赋值
                lastX = centerX;
                lastY = centerY;
            }

            //这里绘制最后一个点和和点连接的线
            if (mPatternInProgress && anyCircles) {
                currentPath.rewind();
                currentPath.moveTo(lastX, lastY);
                currentPath.lineTo(mInProgressX, mInProgressY);
                //把线连接上
                canvas.drawPath(currentPath, mPathPaint);
            }
        }

        //循环画点
        for (int i = 0; i < sDotCount; i++) {
            float centerY = getCenterYForRow(i);//排
            for (int j = 0; j < sDotCount; j++) {
                DotState dotState = mDotStates[i][j];
                float centerX = getCenterXForColumn(j);
                float size = dotState.mSize * dotState.mScale;
                float translationY = dotState.mTranslateY;
                drawCircle(canvas, (int) centerX, (int) centerY + translationY,
                        size, drawLookupTable[i][j], dotState.mAlpha);
            }
        }
    }

drawCircle方法

private void drawCircle(Canvas canvas, float centerX, float centerY,
                            float size, boolean partOfPattern, float alpha) {
        //如果隐藏模式则不走
        if (partOfPattern && !isInStealthMode()) {
            //设置点外环的颜色
            mRingPaint.setColor(getCurrentDotStrokeColor(partOfPattern));
            //画点的外环
            canvas.drawCircle(centerX, centerY, size, mRingPaint);
        }
        //画点
        mDotPaint.setColor(getCurrentDotStateColor(partOfPattern));
        mDotPaint.setAlpha((int) (alpha * 255));
        canvas.drawCircle(centerX, centerY, size / 2, mDotPaint);
    }

在onDraw方法中对画什么颜色的点,什么颜色的线都做了判断。
至此整个绘制过程就算完成了,剩下的一些颜色的判断,各个状态的回调,各个模式都比较简单,就不再这里特别说明了。

贡献

这个库是从Aritra Roy的PatternLockView获取并添加了一些改进使其更加灵活,如果您发现bug或想改进它的任何方面,可以自由地用拉请求进行贡献

源码下载
APK下载

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值