(4.2.42)手势解锁之九宫格控件PatternLockView

gitHub地址
中文介绍

这里写图片描述

/**
 * Displays a powerful, customizable and Material Design complaint pattern lock in the screen which
 * can be used to lock any Activity or Fragment from the user
 */
public class PatternLockView extends View {

    /**
     * Represents the aspect ratio for the View
     */
    @IntDef({ASPECT_RATIO_SQUARE, ASPECT_RATIO_WIDTH_BIAS, ASPECT_RATIO_HEIGHT_BIAS})
    @Retention(RetentionPolicy.SOURCE)
    public @interface AspectRatio {
        // Width and height will be same. Minimum of width and height
        int ASPECT_RATIO_SQUARE = 0;
        // Width will be fixed. The height will be the minimum of width and height
        int ASPECT_RATIO_WIDTH_BIAS = 1;
        // Height will be fixed. The width will be the minimum of width and height
        int ASPECT_RATIO_HEIGHT_BIAS = 2;
    }

    /**
     * Represents the different modes in which this view can be represented
     */
    @IntDef({CORRECT, AUTO_DRAW, WRONG})
    @Retention(RetentionPolicy.SOURCE)
    public @interface PatternViewMode {
        /**
         * This state represents a correctly drawn pattern by the user. The color of the path and
         * the dots both would be changed to this color.
         * <p>
         * (NOTE - Consider showing this state in a friendly color)
         */
        int CORRECT = 0;
        /**
         * Automatically draw the pattern for demo or tutorial purposes.
         */
        int AUTO_DRAW = 1;
        /**
         * This state represents a wrongly drawn pattern by the user. The color of the path and
         * the dots both would be changed to this color.
         * <p>
         * (NOTE - Consider showing this state in an attention-seeking color)
         */
        int WRONG = 2;
    }

    private static final int DEFAULT_PATTERN_DOT_COUNT = 3;
    private static final boolean PROFILE_DRAWING = false;

    /**
     * The time (in millis) spend in animating each circle of a lock pattern if
     * the animating mode is set. The entire animation should take this constant
     * the length of the pattern to complete.
     */
    private static final int MILLIS_PER_CIRCLE_ANIMATING = 700;

    // Amount of time (in millis) spent to animate a dot
    private static final int DEFAULT_DOT_ANIMATION_DURATION = 190;
    // Amount of time (in millis) spent to animate a path ends
    private static final int DEFAULT_PATH_END_ANIMATION_DURATION = 100;
    // This can be used to avoid updating the display for very small motions or noisy panels
    private static final float DEFAULT_DRAG_THRESHOLD = 0.0f;

    private DotState[][] mDotStates;//点状态标示矩阵
    private int mPatternSize;//总共的宫格数量
    private boolean mDrawingProfilingStarted = false;
    private long mAnimatingPeriodStart;
    private float mHitFactor = 0.6f;//点击响应占宫格的比例

    // Made static so that the static inner class can use it
    private static int sDotCount;//一行的点数

    private boolean mAspectRatioEnabled;//是否按比例缩放View
    private int mAspectRatio;//以宽、高优先缩放

    private int mNormalStateColor;//基本色彩
    private int mWrongStateColor;//错误色彩
    private int mCorrectStateColor;//正确色彩

    private int mPathWidth;//路径线条的宽度

    private int mDotNormalSize;//正常的点大小
    private int mDotSelectedSize;//选中时的点大小

    private int mDotAnimationDuration;//点动画时长
    private int mPathEndAnimationDuration;//线动画时长

    private Paint mDotPaint; //点画笔
    private Paint mPathPaint;//路径画笔

    private float mViewWidth;//一个宫格的宽高
    private float mViewHeight;

    private int mPatternViewMode = CORRECT;//当前的模式
    private boolean mInputEnabled = true;//是否可编辑:true时可绘制手势
    private boolean mInStealthMode = false;//是否在隐秘模式:true不画出来线条
    private boolean mEnableHapticFeedback = true;//路径绘制过程中,是否震动

    private List<PatternLockViewListener> mPatternListeners;
    // The pattern represented as a list of connected {@link Dot}
    private ArrayList<Dot> mPattern;//链接中的路径点

    /**
     * Lookup table for the dots of the pattern we are currently drawing.
     * This will be the dots of the complete pattern unless we are animating,
     * in which case we use this to hold the dots we are drawing for the in
     * progress animation.
     */
    private boolean[][] mPatternDrawLookup; // 已选择的点所对应的 矩阵标示

    private boolean mPatternInProgress = false;// 标示是否在 手势绘制过程中
    private float mInProgressX = -1;//最终触摸位置:绘制断头线
    private float mInProgressY = -1;//最终触摸位置:绘制断头线

    private final Path mCurrentPath = new Path();
    private final Rect mInvalidate = new Rect();
    private final Rect mTempInvalidateRect = new Rect();

    private Interpolator mFastOutSlowInInterpolator;//动画 加速器
    private Interpolator mLinearOutSlowInInterpolator;//动画 加速器

    public PatternLockView(Context context) {
        this(context, null);
    }

    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);
            mAspectRatioEnabled = typedArray.getBoolean(R.styleable.PatternLockView_aspectRatioEnabled,
                    false);
            mAspectRatio = typedArray.getInt(R.styleable.PatternLockView_aspectRatio,
                    ASPECT_RATIO_SQUARE);
            mPathWidth = (int) typedArray.getDimension(R.styleable.PatternLockView_pathWidth,
                    ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_path_width));
            mNormalStateColor = typedArray.getColor(R.styleable.PatternLockView_normalStateColor,
                    ResourceUtils.getColor(getContext(), R.color.white));
            mCorrectStateColor = typedArray.getColor(R.styleable.PatternLockView_correctStateColor,
                    ResourceUtils.getColor(getContext(), R.color.white));
            mWrongStateColor = typedArray.getColor(R.styleable.PatternLockView_wrongStateColor,
                    ResourceUtils.getColor(getContext(), R.color.pomegranate));
            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);
            mPathEndAnimationDuration = typedArray.getInt(R.styleable.PatternLockView_pathEndAnimationDuration,
                    DEFAULT_PATH_END_ANIMATION_DURATION);
        } finally {
            typedArray.recycle();
        }

        // The pattern will always be symmetrical
        mPatternSize = sDotCount * sDotCount;
        mPattern = new ArrayList<>(mPatternSize);
        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();
    }

    private void initView() {
        setClickable(true);

        mPathPaint = new Paint();
        mPathPaint.setAntiAlias(true);
        mPathPaint.setDither(true);
        mPathPaint.setColor(mNormalStateColor);
        mPathPaint.setStyle(Paint.Style.STROKE);
        mPathPaint.setStrokeJoin(Paint.Join.ROUND);
        mPathPaint.setStrokeCap(Paint.Cap.ROUND);
        mPathPaint.setStrokeWidth(mPathWidth);

        mDotPaint = new Paint();
        mDotPaint.setAntiAlias(true);
        mDotPaint.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);
        }
    }

    /**************************************一、测量过程******************************************/
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (!mAspectRatioEnabled) {
            return;
        }

        int oldWidth = resolveMeasured(widthMeasureSpec, getSuggestedMinimumWidth());
        int oldHeight = resolveMeasured(heightMeasureSpec, getSuggestedMinimumHeight());

        int newWidth;
        int newHeight;
        switch (mAspectRatio) {
            case ASPECT_RATIO_SQUARE:
                newWidth = newHeight = Math.min(oldWidth, oldHeight);
                break;
            case ASPECT_RATIO_WIDTH_BIAS:
                newWidth = oldWidth;
                newHeight = Math.min(oldWidth, oldHeight);
                break;

            case ASPECT_RATIO_HEIGHT_BIAS:
                newWidth = Math.min(oldWidth, oldHeight);
                newHeight = oldHeight;
                break;

            default:
                throw new IllegalStateException("Unknown aspect ratio");
        }
        setMeasuredDimension(newWidth, newHeight);
    }

    private int resolveMeasured(int measureSpec, int desired) {
        int result;
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (MeasureSpec.getMode(measureSpec)) {
            case MeasureSpec.UNSPECIFIED:
                result = desired;
                break;
            case MeasureSpec.AT_MOST:
                result = Math.max(specSize, desired);
                break;
            case MeasureSpec.EXACTLY:
            default:
                result = specSize;
        }
        return result;
    }

    /**************************************二、布局过程******************************************/
    @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;
    }

    /**************************************三、绘制过程******************************************/
    @Override
    protected void onDraw(Canvas canvas) {
        ArrayList<Dot> pattern = mPattern;
        int patternSize = pattern.size();
        boolean[][] drawLookupTable = mPatternDrawLookup;// 已选择的点所对应的 矩阵标示

        if (mPatternViewMode == AUTO_DRAW) {
            int oneCycle = (patternSize + 1) * MILLIS_PER_CIRCLE_ANIMATING;
            int spotInCycle = (int) (SystemClock.elapsedRealtime() - mAnimatingPeriodStart)
                    % oneCycle;
            int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;

            clearPatternDrawLookup();
            for (int i = 0; i < numCircles; i++) {
                Dot dot = pattern.get(i);
                drawLookupTable[dot.mRow][dot.mColumn] = true;
            }

            boolean needToUpdateInProgressPoint = numCircles > 0
                    && numCircles < patternSize;

            if (needToUpdateInProgressPoint) {
                float percentageOfNextCircle = ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING))
                        / MILLIS_PER_CIRCLE_ANIMATING;

                Dot currentDot = pattern.get(numCircles - 1);
                float centerX = getCenterXForColumn(currentDot.mColumn);
                float centerY = getCenterYForRow(currentDot.mRow);

                Dot nextDot = pattern.get(numCircles);
                float dx = percentageOfNextCircle
                        * (getCenterXForColumn(nextDot.mColumn) - centerX);
                float dy = percentageOfNextCircle
                        * (getCenterYForRow(nextDot.mRow) - centerY);
                mInProgressX = centerX + dx;
                mInProgressY = centerY + dy;
            }
            invalidate();
        }

        Path currentPath = mCurrentPath;
        currentPath.rewind();//清除掉path里的线条和曲线,但是会保留内部的数据结构以便重用;

        //【】绘制点
        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);//x
                float size = dotState.mSize * dotState.mScale;//直径
                float translationY = dotState.mTranslateY;//y的偏移量
                drawCircle(canvas, centerX
                        , (int) centerY + translationY
                        , size
                        , Color.YELLOW
                        , 255 );
                drawCircle(canvas, (int) centerX, (int) centerY + translationY,
                        size, drawLookupTable[i][j], dotState.mAlpha);

            }
        }

        //【】绘制路径 Draw the path of the pattern (unless we are in stealth mode)
        boolean drawPath = !mInStealthMode;
        if (drawPath) {
            mPathPaint.setColor(getCurrentColor(true));

            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]) {
                    break;
                }
                anyCircles = true;

                float centerX = getCenterXForColumn(dot.mColumn);
                float centerY = getCenterYForRow(dot.mRow);
                if (i != 0) {
                    DotState state = mDotStates[dot.mRow][dot.mColumn];
                    currentPath.rewind();//清除掉path里的线条和曲线,但是会保留内部的数据结构以便重用;
                    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 || mPatternViewMode == AUTO_DRAW)
                    && anyCircles) {
                currentPath.rewind();
                currentPath.moveTo(lastX, lastY);
                currentPath.lineTo(mInProgressX, mInProgressY);

                mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(
                        mInProgressX, mInProgressY, lastX, lastY) * 255f));
                canvas.drawPath(currentPath, mPathPaint);
            }
        }
    }

    private float calculateLastSegmentAlpha(float x, float y, float lastX,
                                            float lastY) {
        float diffX = x - lastX;
        float diffY = y - lastY;
        float dist = (float) Math.sqrt(diffX * diffX + diffY * diffY);
        float fraction = dist / mViewWidth;
        return Math.min(1f, Math.max(0f, (fraction - 0.3f) * 4f));
    }

    /******************************************四、不保留活动*****************************************************/

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        return new SavedState(superState,
                PatternLockUtils.patternToString(this, mPattern),
                mPatternViewMode, mInputEnabled, mInStealthMode,
                mEnableHapticFeedback);
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        final SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());
        setPattern(CORRECT,
                PatternLockUtils.stringToPattern(this, savedState.getSerializedPattern()));
        mPatternViewMode = savedState.getDisplayMode();
        mInputEnabled = savedState.isInputEnabled();
        mInStealthMode = savedState.isInStealthMode();
        mEnableHapticFeedback = savedState.isTactileFeedbackEnabled();
    }

    /**
     * The parcelable for saving and restoring a lock pattern view
     */
    private static class SavedState extends BaseSavedState {

        private final String mSerializedPattern;
        private final int mDisplayMode;
        private final boolean mInputEnabled;
        private final boolean mInStealthMode;
        private final boolean mTactileFeedbackEnabled;

        /**
         * Constructor called from {@link PatternLockView#onSaveInstanceState()}
         */
        private SavedState(Parcelable superState, String serializedPattern,
                           int displayMode, boolean inputEnabled, boolean inStealthMode,
                           boolean tactileFeedbackEnabled) {
            super(superState);

            mSerializedPattern = serializedPattern;
            mDisplayMode = displayMode;
            mInputEnabled = inputEnabled;
            mInStealthMode = inStealthMode;
            mTactileFeedbackEnabled = tactileFeedbackEnabled;
        }

        /**
         * Constructor called from {@link #CREATOR}
         */
        private SavedState(Parcel in) {
            super(in);

            mSerializedPattern = in.readString();
            mDisplayMode = in.readInt();
            mInputEnabled = (Boolean) in.readValue(null);
            mInStealthMode = (Boolean) in.readValue(null);
            mTactileFeedbackEnabled = (Boolean) in.readValue(null);
        }

        public String getSerializedPattern() {
            return mSerializedPattern;
        }

        public int getDisplayMode() {
            return mDisplayMode;
        }

        public boolean isInputEnabled() {
            return mInputEnabled;
        }

        public boolean isInStealthMode() {
            return mInStealthMode;
        }

        public boolean isTactileFeedbackEnabled() {
            return mTactileFeedbackEnabled;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeString(mSerializedPattern);
            dest.writeInt(mDisplayMode);
            dest.writeValue(mInputEnabled);
            dest.writeValue(mInStealthMode);
            dest.writeValue(mTactileFeedbackEnabled);
        }

        @SuppressWarnings("unused")
        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {

            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }
            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

    /*****************************************五、控制********************************************/
    @Override
    public boolean onHoverEvent(MotionEvent event) {
        if (((AccessibilityManager) getContext().getSystemService(
                Context.ACCESSIBILITY_SERVICE)).isTouchExplorationEnabled()) {
            final int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_HOVER_ENTER:
                    event.setAction(MotionEvent.ACTION_DOWN);
                    break;
                case MotionEvent.ACTION_HOVER_MOVE:
                    event.setAction(MotionEvent.ACTION_MOVE);
                    break;
                case MotionEvent.ACTION_HOVER_EXIT:
                    event.setAction(MotionEvent.ACTION_UP);
                    break;
            }
            onTouchEvent(event);
            event.setAction(action);
        }
        return super.onHoverEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        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();

                if (PROFILE_DRAWING) {
                    if (mDrawingProfilingStarted) {
                        Debug.stopMethodTracing();
                        mDrawingProfilingStarted = false;
                    }
                }
                return true;
        }
        return false;
    }

    /**
     * 5.1 down事件响应
     * 1. 清理屏幕
     * 2. 获取当前手指的坐标,调用detectAndAddHit(float x, float y)判断落点宫格,加入全局选中列表
     * 3. 发送通知与局部刷新
     * @param event
     */
    private void handleActionDown(MotionEvent event) {
        resetPattern();
        float x = event.getX();
        float y = event.getY();

        Dot hitDot = detectAndAddHit(x, y);

        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;

            invalidate((int) (startX - widthOffset),
                    (int) (startY - heightOffset),
                    (int) (startX + widthOffset), (int) (startY + heightOffset));
        }
        mInProgressX = x;
        mInProgressY = y;
        if (PROFILE_DRAWING) {
            if (!mDrawingProfilingStarted) {
                Debug.startMethodTracing("PatternLockDrawing");
                mDrawingProfilingStarted = true;
            }
        }
    }

    /**
     * 判断落点位置是否命中新宫格:
     * 如果命中,则进一步判断  本次命中的新宫格  和 上一次宫格 间是否有未链接的宫格
     *          没有间隙宫格,则仅将 本次命中的新宫格  加入 全局链路中
     *          有间隙宫格,则默认也加入 全局链路中
     *
     * 未命中则返回null
     */
    private Dot detectAndAddHit(float x, float y) {
        final Dot dot = checkForNewHit(x, y);
        if (dot != null) {
            // Check for gaps in existing pattern
            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;

                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);
            }
            //判断间隙宫格是否在 已选择链路中
            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;
    }

    //判断命中宫格: x,y命中的,并且不在已记录链表中的宫格
    //根据getRowHit(float y)与getColumnHit(float x)返回的行、列判断是否是新的图形点,如果是返回新点,否则返回 null
    private Dot checkForNewHit(float x, float y) {
        final int rowHit = getRowHit(y);
        if (rowHit < 0) {
            return null;
        }
        final int columnHit = getColumnHit(x);
        if (columnHit < 0) {
            return null;
        }

        if (mPatternDrawLookup[rowHit][columnHit]) {
            return null;
        }
        return Dot.of(rowHit, columnHit);
    }
    //遍历所有图形点行高,寻找坐标 y 在哪行图案点的行高范围内
    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 在哪列图案点的列宽范围内
    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;
    }

    //将目标点加入已选择链路,并开启一次动画
    private void addCellToPattern(Dot newDot) {
        mPatternDrawLookup[newDot.mRow][newDot.mColumn] = true;
        mPattern.add(newDot);
        if (!mInStealthMode) {
            startDotSelectedAnimation(newDot);
        }
        notifyPatternProgress();
    }


    /**
     * 检查手指移动过程中每一个点的坐标,判断
     * : 如果 pattern list 不为空,则把最后一个图形点的坐标与当前手指坐标的区域进行局部刷新
     * : 如果在移动过程中加入了新的图形点则以此点坐标继续局部刷新
     * @param event
     */
    private void handleActionMove(MotionEvent event) {
        float radius = mPathWidth;
        int historySize = event.getHistorySize();//历史个数
        mTempInvalidateRect.setEmpty();
        boolean invalidateNow = false;

        //遍历 移动的历史位置
        for (int i = 0; i < historySize + 1; i++) {
            float x = i < historySize ? event.getHistoricalX(i) : event
                    .getX();
            float y = i < historySize ? event.getHistoricalY(i) : event
                    .getY();

            //历史位置 是否命中某个宫格
            Dot hitDot = detectAndAddHit(x, y);

            int patternSize = mPattern.size();
            if (hitDot != null && patternSize == 1) {
                //当前链路没有宫格  则记录 开启了一次绘制
                mPatternInProgress = true;
                notifyPatternStarted();
            }

            // 是否要立即绘制:判断当前落点 和上一次的位置距离
            float dx = Math.abs(x - mInProgressX);
            float dy = Math.abs(y - mInProgressY);
            if (dx > DEFAULT_DRAG_THRESHOLD || dy > DEFAULT_DRAG_THRESHOLD) {
                invalidateNow = true;
            }

            //已经在 一次流程中的
            if (mPatternInProgress && patternSize > 0) {
                final ArrayList<Dot> pattern = mPattern;
                final Dot lastDot = pattern.get(patternSize - 1);
                float lastCellCenterX = getCenterXForColumn(lastDot.mColumn);
                float lastCellCenterY = getCenterYForRow(lastDot.mRow);

                // Adjust for drawn segment from last cell to (x,y). Radius
                // accounts for line width.
                float left = Math.min(lastCellCenterX, x) - radius;
                float right = Math.max(lastCellCenterX, x) + radius;
                float top = Math.min(lastCellCenterY, y) - radius;
                float bottom = Math.max(lastCellCenterY, y) + radius;

                // Invalidate between the pattern's new cell and the pattern's
                // previous cell
                if (hitDot != null) {
                    float width = mViewWidth * 0.5f;
                    float height = mViewHeight * 0.5f;
                    float hitCellCenterX = getCenterXForColumn(hitDot.mColumn);
                    float hitCellCenterY = getCenterYForRow(hitDot.mRow);

                    left = Math.min(hitCellCenterX - width, left);
                    right = Math.max(hitCellCenterX + width, right);
                    top = Math.min(hitCellCenterY - height, top);
                    bottom = Math.max(hitCellCenterY + height, bottom);
                }

                // Invalidate between the pattern's last cell and the previous
                // location
                mTempInvalidateRect.union(Math.round(left), Math.round(top),
                        Math.round(right), Math.round(bottom));
            }
        }

        //更新 最后一次触摸点位置
        mInProgressX = event.getX();
        mInProgressY = event.getY();

        // To save updates, we only invalidate if the user moved beyond a
        // certain amount.
        //仅在用户移动了一定的位置后绘制
        if (invalidateNow) {
            mInvalidate.union(mTempInvalidateRect);
            invalidate(mInvalidate);
            mInvalidate.set(mTempInvalidateRect);
        }
    }


    /**
     * 检查 pattern list 如果不为空则停止添加,发送完成消息,全局刷新
     * @param event
     */
    private void handleActionUp(MotionEvent event) {
        // Report pattern detected
        if (!mPattern.isEmpty()) {
            mPatternInProgress = false;
            cancelLineAnimations();
            notifyPatternDetected();
            invalidate();
        }
        if (PROFILE_DRAWING) {
            if (mDrawingProfilingStarted) {
                Debug.stopMethodTracing();
                mDrawingProfilingStarted = false;
            }
        }
    }

    private void cancelLineAnimations() {
        for (int i = 0; i < sDotCount; i++) {
            for (int j = 0; j < sDotCount; j++) {
                DotState state = mDotStates[i][j];
                if (state.mLineAnimator != null) {
                    state.mLineAnimator.cancel();
                    state.mLineEndX = Float.MIN_VALUE;
                    state.mLineEndY = Float.MIN_VALUE;
                }
            }
        }
    }


    /*****************************************动画类******************************************************/
    private void startDotSelectedAnimation(Dot dot) {
        final DotState dotState = mDotStates[dot.mRow][dot.mColumn];
        startSizeAnimation(mDotNormalSize, mDotSelectedSize, mDotAnimationDuration,
                mLinearOutSlowInInterpolator, dotState, new Runnable() {

                    @Override
                    public void run() {
                        startSizeAnimation(mDotSelectedSize, mDotNormalSize, mDotAnimationDuration,
                                mFastOutSlowInInterpolator, dotState, null);
                    }
                });
        startLineEndAnimation(dotState, mInProgressX, mInProgressY,
                getCenterXForColumn(dot.mColumn), getCenterYForRow(dot.mRow));
    }

    private void startLineEndAnimation(final DotState state,
                                       final float startX, final float startY, final float targetX,
                                       final float targetY) {
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float t = (Float) animation.getAnimatedValue();
                state.mLineEndX = (1 - t) * startX + t * targetX;
                state.mLineEndY = (1 - t) * startY + t * targetY;
                invalidate();
            }

        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {

            @Override
            public void onAnimationEnd(Animator animation) {
                state.mLineAnimator = null;
            }

        });
        valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
        valueAnimator.setDuration(mPathEndAnimationDuration);
        valueAnimator.start();
        state.mLineAnimator = valueAnimator;
    }

    private void startSizeAnimation(float start, float end, long duration,
                                    Interpolator interpolator, final DotState state,
                                    final Runnable endRunnable) {
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                state.mSize = (Float) animation.getAnimatedValue();
                invalidate();
            }

        });
        if (endRunnable != null) {
            valueAnimator.addListener(new AnimatorListenerAdapter() {

                @Override
                public void onAnimationEnd(Animator animation) {
                    if (endRunnable != null) {
                        endRunnable.run();
                    }
                }
            });
        }
        valueAnimator.setInterpolator(interpolator);
        valueAnimator.setDuration(duration);
        valueAnimator.start();
    }


    /***************************************控制类utils*********************************************/
    //清空路径并重绘
    private void resetPattern() {
        mPattern.clear();//清除当前路径点
        clearPatternDrawLookup();//已选择的点所对应的 矩阵标示  全部置为false标示未选中
        mPatternViewMode = CORRECT;
        invalidate();
    }

    private void clearPatternDrawLookup() {
        for (int i = 0; i < sDotCount; i++) {
            for (int j = 0; j < sDotCount; j++) {
                mPatternDrawLookup[i][j] = false;
            }
        }
    }

    private void notifyPatternProgress() {
        sendAccessEvent(R.string.message_pattern_dot_added);
        notifyListenersProgress(mPattern);
    }

    private void notifyPatternStarted() {
        sendAccessEvent(R.string.message_pattern_started);
        notifyListenersStarted();
    }

    private void notifyPatternDetected() {
        sendAccessEvent(R.string.message_pattern_detected);
        notifyListenersComplete(mPattern);
    }

    private void notifyPatternCleared() {
        sendAccessEvent(R.string.message_pattern_cleared);
        notifyListenersCleared();
    }

    private void sendAccessEvent(int resId) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
            setContentDescription(getContext().getString(resId));
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
            setContentDescription(null);
        } else {
            announceForAccessibility(getContext().getString(resId));
        }
    }

    private void notifyListenersStarted() {
        for (PatternLockViewListener patternListener : mPatternListeners) {
            if (patternListener != null) {
                patternListener.onStarted();
            }
        }
    }

    private void notifyListenersProgress(List<Dot> pattern) {
        for (PatternLockViewListener patternListener : mPatternListeners) {
            if (patternListener != null) {
                patternListener.onProgress(pattern);
            }
        }
    }

    private void notifyListenersComplete(List<Dot> pattern) {
        for (PatternLockViewListener patternListener : mPatternListeners) {
            if (patternListener != null) {
                patternListener.onComplete(pattern);
            }
        }
    }

    private void notifyListenersCleared() {
        for (PatternLockViewListener patternListener : mPatternListeners) {
            if (patternListener != null) {
                patternListener.onCleared();
            }
        }
    }


    /*************************************数据结构***********************************************/

    /**
     * Represents a cell in the matrix of the pattern view
     */
    public static class Dot implements Parcelable {

        private int mRow;
        private int mColumn;
        private static Dot[][] sDots;

        static {
            sDots = new Dot[sDotCount][sDotCount];

            // Initializing the dots
            for (int i = 0; i < sDotCount; i++) {
                for (int j = 0; j < sDotCount; j++) {
                    sDots[i][j] = new Dot(i, j);
                }
            }
        }

        private Dot(int row, int column) {
            checkRange(row, column);
            this.mRow = row;
            this.mColumn = column;
        }

        /**
         * Gets the identifier of the dot. It is counted from left to right, top to bottom of the
         * matrix, starting by zero
         */
        public int getId() {
            return mRow * sDotCount + mColumn;
        }

        public int getRow() {
            return mRow;
        }

        public int getColumn() {
            return mColumn;
        }

        /**
         * @param row    The mRow of the cell.
         * @param column The mColumn of the cell.
         */
        public static synchronized Dot of(int row, int column) {
            checkRange(row, column);
            return sDots[row][column];
        }

        /**
         * Gets a cell from its identifier
         */
        public static synchronized Dot of(int id) {
            return of(id / sDotCount, id % sDotCount);
        }

        private static void checkRange(int row, int column) {
            if (row < 0 || row > sDotCount - 1) {
                throw new IllegalArgumentException("mRow must be in range 0-"
                        + (sDotCount - 1));
            }
            if (column < 0 || column > sDotCount - 1) {
                throw new IllegalArgumentException("mColumn must be in range 0-"
                        + (sDotCount - 1));
            }
        }

        @Override
        public String toString() {
            return "(Row = " + mRow + ", Col = " + mColumn + ")";
        }

        @Override
        public boolean equals(Object object) {
            if (object instanceof Dot) {
                return mColumn == ((Dot) object).mColumn
                        && mRow == ((Dot) object).mRow;
            }
            return super.equals(object);
        }

        @Override
        public int hashCode() {
            int result = mRow;
            result = 31 * result + mColumn;
            return result;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mColumn);
            dest.writeInt(mRow);
        }

        public static final Creator<Dot> CREATOR = new Creator<Dot>() {

            @Override
            public Dot createFromParcel(Parcel in) {
                return new Dot(in);
            }
            @Override
            public Dot[] newArray(int size) {
                return new Dot[size];
            }
        };

        private Dot(Parcel in) {
            mColumn = in.readInt();
            mRow = in.readInt();
        }
    }



    public static class DotState {
        float mScale = 1.0f;
        float mTranslateY = 0.0f;
        float mAlpha = 1.0f;
        float mSize;
        float mLineEndX = Float.MIN_VALUE;
        float mLineEndY = Float.MIN_VALUE;
        ValueAnimator mLineAnimator;
    }


    /***********************************绘制 Utils**********************************************/
    //获取指定行的圆心所在Y坐标
    private float getCenterYForRow(int row) {
        return getPaddingTop() + row * mViewHeight + mViewHeight / 2f;
    }
    //获取指定列的圆心所在x坐标
    private float getCenterXForColumn(int column) {
        return getPaddingLeft() + column * mViewWidth + mViewWidth / 2f;
    }

    // 绘制圆
    //partOfPattern 该点是否在路径中
    private void drawCircle(Canvas canvas, float centerX, float centerY,
                            float size, boolean partOfPattern, float alpha) {
        drawCircle(canvas, centerX, centerY, size / 2, getCurrentColor(partOfPattern), (int) (alpha * 255) );
    }

    private void drawCircle(Canvas canvas, float centerX, float centerY,
                            float raduis, int color, int alpha) {
        mDotPaint.setColor(color);
        mDotPaint.setAlpha(alpha);
        canvas.drawCircle(centerX, centerY, raduis, mDotPaint);
    }

    // 获取目标圆的颜色
    //partOfPattern 该点是否在路径中:不在的话,返回默认色彩;在的话,根据是错误还是正确,进一步区分
    private int getCurrentColor(boolean partOfPattern) {
        if (!partOfPattern || mInStealthMode || mPatternInProgress) {
            return mNormalStateColor;
        } else if (mPatternViewMode == WRONG) {
            return mWrongStateColor;
        } else if (mPatternViewMode == CORRECT
                || mPatternViewMode == AUTO_DRAW) {
            return mCorrectStateColor;
        } else {
            throw new IllegalStateException("Unknown view mode " + mPatternViewMode);
        }
    }

    /***********************************get | set**********************************************/


    /**
     * Returns the list of dots in the current selected pattern. This list is independent of the
     * internal pattern dot list
     */
    @SuppressWarnings("unchecked")
    public List<Dot> getPattern() {
        return (List<Dot>) mPattern.clone();
    }

    @PatternViewMode
    public int getPatternViewMode() {
        return mPatternViewMode;
    }

    public boolean isInStealthMode() {
        return mInStealthMode;
    }

    public boolean isTactileFeedbackEnabled() {
        return mEnableHapticFeedback;
    }

    public boolean isInputEnabled() {
        return mInputEnabled;
    }

    public int getDotCount() {
        return sDotCount;
    }

    public boolean isAspectRatioEnabled() {
        return mAspectRatioEnabled;
    }

    @AspectRatio
    public int getAspectRatio() {
        return mAspectRatio;
    }

    public int getNormalStateColor() {
        return mNormalStateColor;
    }

    public int getWrongStateColor() {
        return mWrongStateColor;
    }

    public int getCorrectStateColor() {
        return mCorrectStateColor;
    }

    public int getPathWidth() {
        return mPathWidth;
    }

    public int getDotNormalSize() {
        return mDotNormalSize;
    }

    public int getDotSelectedSize() {
        return mDotSelectedSize;
    }

    public int getPatternSize() {
        return mPatternSize;
    }

    public int getDotAnimationDuration() {
        return mDotAnimationDuration;
    }

    public int getPathEndAnimationDuration() {
        return mPathEndAnimationDuration;
    }

    /**
     * Set the pattern explicitly rather than waiting for the user to input a
     * pattern. You can use this for help or demo purposes
     *
     * @param patternViewMode The mode in which the pattern should be displayed
     * @param pattern         The pattern
     */
    public void setPattern(@PatternViewMode int patternViewMode, List<Dot> pattern) {
        mPattern.clear();
        mPattern.addAll(pattern);
        clearPatternDrawLookup();
        for (Dot dot : pattern) {
            mPatternDrawLookup[dot.mRow][dot.mColumn] = true;
        }
        setViewMode(patternViewMode);
    }

    /**
     * Set the display mode of the current pattern. This can be useful, for
     * instance, after detecting a pattern to tell this view whether change the
     * in progress result to correct or wrong.
     */
    public void setViewMode(@PatternViewMode int patternViewMode) {
        mPatternViewMode = patternViewMode;
        if (patternViewMode == AUTO_DRAW) {
            if (mPattern.size() == 0) {
                throw new IllegalStateException(
                        "you must have a pattern to "
                                + "animate if you want to set the display mode to animate");
            }
            mAnimatingPeriodStart = SystemClock.elapsedRealtime();
            final Dot first = mPattern.get(0);
            mInProgressX = getCenterXForColumn(first.mColumn);
            mInProgressY = getCenterYForRow(first.mRow);
            clearPatternDrawLookup();
        }
        invalidate();
    }

    public void setDotCount(int dotCount) {
        sDotCount = dotCount;
        mPatternSize = sDotCount * sDotCount;
        mPattern = new ArrayList<>(mPatternSize);
        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;
            }
        }

        requestLayout();
        invalidate();
    }

    public void setAspectRatioEnabled(boolean aspectRatioEnabled) {
        mAspectRatioEnabled = aspectRatioEnabled;
        requestLayout();
    }

    public void setAspectRatio(@AspectRatio int aspectRatio) {
        mAspectRatio = aspectRatio;
        requestLayout();
    }

    public void setNormalStateColor(@ColorInt int normalStateColor) {
        mNormalStateColor = normalStateColor;
    }

    public void setWrongStateColor(@ColorInt int wrongStateColor) {
        mWrongStateColor = wrongStateColor;
    }

    public void setCorrectStateColor(@ColorInt int correctStateColor) {
        mCorrectStateColor = correctStateColor;
    }

    public void setPathWidth(@Dimension int pathWidth) {
        mPathWidth = pathWidth;

        initView();
        invalidate();
    }

    public void setDotNormalSize(@Dimension int dotNormalSize) {
        mDotNormalSize = dotNormalSize;

        for (int i = 0; i < sDotCount; i++) {
            for (int j = 0; j < sDotCount; j++) {
                mDotStates[i][j] = new DotState();
                mDotStates[i][j].mSize = mDotNormalSize;
            }
        }

        invalidate();
    }

    public void setDotSelectedSize(@Dimension int dotSelectedSize) {
        mDotSelectedSize = dotSelectedSize;
    }

    public void setDotAnimationDuration(int dotAnimationDuration) {
        mDotAnimationDuration = dotAnimationDuration;
        invalidate();
    }

    public void setPathEndAnimationDuration(int pathEndAnimationDuration) {
        mPathEndAnimationDuration = pathEndAnimationDuration;
    }

    /**
     * Set whether the View is in stealth mode. If {@code true}, there will be
     * no visible feedback (path drawing, dot animating, etc) as the user enters the pattern
     */
    public void setInStealthMode(boolean inStealthMode) {
        mInStealthMode = inStealthMode;
    }

    public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) {
        mEnableHapticFeedback = tactileFeedbackEnabled;
    }

    /**
     * Enabled/disables any user input of the view. This can be useful to lock the view temporarily
     * while showing any message to the user so that the user cannot get the view in
     * an unwanted state
     */
    public void setInputEnabled(boolean inputEnabled) {
        mInputEnabled = inputEnabled;
    }

    public void setEnableHapticFeedback(boolean enableHapticFeedback) {
        mEnableHapticFeedback = enableHapticFeedback;
    }

    public void addPatternLockListener(PatternLockViewListener patternListener) {
        mPatternListeners.add(patternListener);
    }

    public void removePatternLockListener(PatternLockViewListener patternListener) {
        mPatternListeners.remove(patternListener);
    }

    public void clearPattern() {
        resetPattern();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值