写一个图案解锁控件

写一个图案解锁控件

虽然网上有很多的关于图案解锁的现成轮子,但是 ,有什么比自己写一个轮子更带劲的事情呢?

首先展示效果:

实现分析

属性分析

应为这是一个自定义控件,网上很多的轮子都是通过替换图片来实现的,但是,我并不想使用图片(最主要,不会切图)。那么我就需要考虑,我应该使用那些自定义属性。

通过观察别人的轮子,发现主要的自定义属性有这些:

  1. 正常情况下的点颜色
  2. 按下时点的颜色
  3. 错误时点的颜色
  4. 连接时线的颜色
  5. 错误时线的颜色

PS:应该还有一个点的半径,但是我想想还是写死了算了。

实现分析

先上一张图:

图有点丑,见谅

图中的9个实心点,就是代表我们所需要绘制的九宫的9个点的位置,整个区域是一个正方形(不要问我为什么是正方形),可以从图中看到水平和竖直的3个点把分别把横竖方向上分成了4部分,我们计算的坐标时每个点取1/4就行了。

行为分析

  1. 在进行连线时,同一个点不能被链接两次
  2. 横,竖,斜三个方向上不能有跳过 。以横向举例,比如选择了第一个点,不能跳过第二个点,而去直接连接第三个点。

代码实现

### 自定义属性

经过上面的分析,我可以开始动手了。

首先定义5个属性:

 <declare-styleable name="PatternDeblockView">
        <!--  按下时的圆的颜色-->
        <attr name="pressed_color" format="color"></attr>
        <!--错误时的圆的颜色-->
        <attr name="error_color" format="color"></attr>
        <!--平常时的颜色-->
        <attr name="normal_color" format="color"></attr>
        <!--错误时的线的颜色-->
        <attr name="error_line_color" format="color"></attr>
        <!--滑动时的线上的颜色-->
        <attr name="pressed_line_color" format="color"></attr>
    </declare-styleable>

当5个属性定义完成后,获取这些属性:

 private void initAttr(Context context, AttributeSet attrs) {
        if (null != attrs) {
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PatternDeblockView);
            int len = array.getIndexCount();
            for (int i = 0; i < len; i++) {
                int attr = array.getIndex(i);
                switch (attr) {
                    case R.styleable.PatternDeblockView_pressed_color:
                        mPressedColor = array.getColor(attr, Color.BLUE);
                        break;
                    case R.styleable.PatternDeblockView_error_color:
                        mErrorColor = array.getColor(attr, Color.RED);
                        break;
                    case R.styleable.PatternDeblockView_normal_color:
                        mNormalColor = array.getColor(attr, Color.GRAY);
                        break;
                    case R.styleable.PatternDeblockView_pressed_line_color:
                        mPressedLineColor = array.getColor(attr, Color.BLUE);
                        break;
                    case R.styleable.PatternDeblockView_error_line_color:
                        mErrorLineColor = array.getColor(attr, Color.RED);
                        break;
                }
            }
        }
        mChoosePoints = new ArrayList<Point>();
        mChoosePassword = new ArrayList<Integer>();
        //设置半径
        mRadius = 40;
        mPaint = new Paint();
        mPaint.setStrokeWidth(5);//设置线宽
        mPaint.setAntiAlias(true);
    }

在获取自定义属性值得同时对一些属性进行设置,在这里我设置了圆的半径和画笔的线宽。该方法需要在重写的构造方法中调用。

测量

自定义属性获取完成后,我需要通过重写onMeasure()方法,获取我当前控件的宽高值:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = measureWidth(widthMeasureSpec);
        height = measureHeight(heightMeasureSpec);
        setMeasuredDimension(width, height);
    }
private int measureHeight(int heightMeasureSpec) {
        int height = 0;
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);

        if (mode == MeasureSpec.EXACTLY) {
            height = size;
        } else {
            height = 200;
        }
        return height;
    }

measureWidth()measureHeight()方法类似,这几个方法都是基本通用的形式。

绘制

这一步是两个关键部分的其中一个。

在重写的onDraw()方法中,我需要把九宫格的九宫点,按照之前的分析画出来:

protected void onDraw(Canvas canvas) {
        //没有进行初始化
        if (mPoints == null) {
            // 进行初始化
            initPaints();
        }

        drawPoints(canvas, mPoints);
        drawLine(canvas, movingX, movingY);

        super.onDraw(canvas);
    }

上来首先,对九个点有没有初始化进行判断,如果已经初始化了,就直接画点和画线,如果没有初始化,先进行初始化操作:

   private void initPaints() {

        float offsetX = 0;
        float offsetY = 0;

        min = Math.min(width, height);//获取最小值
        if (width >= height) {//如果宽比高大
            offsetX = (width - height) / 2;
        } else {//如果宽比高小
            offsetY = (height - width) / 2;
        }
        mPoints = new Point[3][3];

        //初始化完成
        mPoints[0][0] = new Point(offsetX + min / 4, offsetY + min / 4, 1);
        mPoints[0][1] = new Point(offsetX + min / 2, offsetY + min / 4, 2);
        mPoints[0][2] = new Point(offsetX + min - min / 4, offsetY + min / 4, 3);

        mPoints[1][0] = new Point(offsetX + min / 4, offsetY + min / 2, 4);
        mPoints[1][1] = new Point(offsetX + min / 2, offsetY + min / 2, 5);
        mPoints[1][2] = new Point(offsetX + min - min / 4, offsetY + min / 2, 6);

        mPoints[2][0] = new Point(offsetX + min / 4, offsetY + min - min / 4, 7);
        mPoints[2][1] = new Point(offsetX + min / 2, offsetY + min - min / 4, 8);
        mPoints[2][2] = new Point(offsetX + min - min / 4, offsetY + min - min / 4, 9);
    }

通过使用3*3的二维数组进行点的初始化,按照下图所示进行说明

由于我们要把九宫放在我们自定义控件展示的中间位置,因此需要获取控件实际的宽和高并获取最小的一个,如上图所示的情况,由于height大于width,因此,在y方向要加上偏移,由于加上偏移后要使得九宫位于中间,因此y的偏移量为(height-widht)/2。反之。x方向的偏移为(width-height)/2

在计算完偏移后就能通过4等分的原则,得到所用点的具体坐标。把9点的位置计算完成后,进行画点:

    private void drawPoints(Canvas canvas, Point[][] mPoints) {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                Point point = mPoints[i][j];
                if (point.state == Point.NORMALSTATE) {
                    mPaint.setColor(mNormalColor);
                } else if (point.state == Point.PRESSEDSTATE) {
                    mPaint.setColor(mPressedColor);
                } else {
                    mPaint.setColor(mErrorColor);
                }
                canvas.drawCircle(point.x, point.y, mRadius, mPaint);
            }
        }
    }

在画点的时候需要注意的是,根据点的状态,绘制不同的颜色。接下的的任务,就是画线了,划线部分,需要结合对Touch事件的处理一起描述:

 private void drawLine(Canvas canvas, float movingX, float movingY) {
        float secondX = 0;
        float secondY = 0;
        float firstX = 0;
        float firstY = 0;
        if (isError) {
            mPaint.setColor(mErrorLineColor);//设置错误时的线颜色
        } else {
            mPaint.setColor(mPressedLineColor);
        }
        int len = mChoosePoints.size();

        if (len >= 1) {
            firstX = mChoosePoints.get(0).x;
            firstY = mChoosePoints.get(0).y;
        }
        //画点中的线
        for (int i = 1; i < len; i++) {
            secondX = mChoosePoints.get(i).x;
            secondY = mChoosePoints.get(i).y;

            canvas.drawLine(firstX, firstY, secondX, secondY, mPaint);
            firstX = secondX;
            firstY = secondY;
        }

        if (!isFinish) {
            //画点到点的线
            if (len >= 1) {
                canvas.drawLine(firstX, firstY, movingX, movingY, mPaint);
            }
        }

    }

首先是把已经选中的点之间用线连接起来。也要注意状态。如果没有移动事件没有结束同时没有点和最后一个点连接起来,需要把最后一个点和当其的手指点击位置连接起来。

处理Touch事件

这部分是重中之重,先上代码:

public boolean onTouchEvent(MotionEvent event) {
        //获得当其移动位置
        movingX = event.getX();
        movingY = event.getY();
        Point point = null;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!isFinish) {
                    point = choosePoints(movingX, movingY);//判断落下的点是否在9个点上
                    if (null != point) {
                        mChoosePoints.add(point);//如果不为空加入选中的集合中
                        mChoosePassword.add(point.index);
                        isChosen = true;// 表示已经与了选择
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                //移动时有两情况需要进考虑,1:在点的范围时需要把点选中,2:重复点不计算,3:一条线上 不能跳过选择
                if (!isFinish) {
                    isRepeat = checkIsRepeat(movingX, movingY);
                    if (!isRepeat) {//如果不在已经选中的点中,判读是否在点阵中
                        point = choosePoints(movingX, movingY);
                        //进行跳过选择
                        if (null != point) {
                            mChoosePoints.add(point);
                            mChoosePassword.add(point.index);
                        }
                        parsePoint();
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                isFinish = true;
                //对点数进行判读 如果连起来的点数少于5设置成
                int len = mChoosePoints.size();
                if (len == 0) {
                    isChosen = false;
                    isFinish = false;
                }
                if (!isPassword) {//不是密码模式
                    if (len < 5) {
                        isError = true;
                        for (int i = 0; i < len; i++) {
                            point = mChoosePoints.get(i);
                            point.state = Point.ERRORSTATE;
                        }
                        if (null != mListener) {
                            mListener.drawMessage(false, null);
                        }
                    } else {
                        if (null != mListener) {
                            mListener.drawMessage(true, mChoosePassword);
                        }
                    }
                } else {//是密码模式
                    if (mChoosePassword.equals(mPassword)) {
                        if (null != mListener) {
                            mListener.enterPass(true);
                        }
                    } else {
                        if (null != mListener) {
                            //把颜色改变
                            len = mChoosePoints.size();
                            for (int i = 0; i < len; i++) {
                                point = mChoosePoints.get(i);
                                point.state = Point.ERRORSTATE;
                            }
                            mListener.enterPass(false);
                        }
                    }
                }
                break;
        }

        postInvalidate();// 进行重绘
        return true;
    }

在该Touch事件中,需要处理3个Action,ACTION_DOWNACTION_MOVEACTION_UP三个事件。

ACTION_DOWN事件中需要处理,当前用户点击的区域是否在点的范围中,如果在范围中,就把该点添加的被选中的集合中。并把选中的状态进行更改。

判断是否在点的范围中也很简单:

 private Point choosePoints(float movingX, float movingY) {
        Log.i(TAG, "movingX:" + movingX + ";;;;;movingY:" + movingY);
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                Point point = mPoints[i][j];
                if (Point.isInPoint(point, movingX, movingY, mRadius)) {
                    //修改点的状态(被选中)
                    point.state = Point.PRESSEDSTATE;
                    return point;
                }
            }
        }
        return null;
    }

ACTION_MOVE事件中需要对下面三种情况进处理。

  1. 在点的范围时需要把点选中。
  2. 重复点不计算。
  3. 一条线上不能跳过选择。、

第一种情况和ACTION_DOWN的处理方式一致,第二种情况处理如下:

private boolean checkIsRepeat(float movingX, float movingY) {
        int len = mChoosePoints.size();
        for (int i = 0; i < len; i++) {
            if (Point.isInPoint(mChoosePoints.get(i), movingX, movingY, mRadius)) {
                return true;
            }
        }
        return false;
    }

也是很简单的循环。

针对第三种情况,我只想到了如下的实现方式,讲道理,应该有其他的方式进行判断:

 private void parsePoint() {
        int len = mChoosePoints.size();
        //如果被选中的点个数不大于1
        if (len <= 1) {
            return;
        }

        //拿到最后最后两个Point
        Point a = mChoosePoints.get(len - 1);
        Point b = mChoosePoints.get(len - 2);

        Point p = null;
        float x = 0;
        float y = 0;

        if (a.x == b.x && Math.abs(a.y - b.y) == min / 2) {//在同一条竖线
            Log.i(TAG, "a.x=b.x");
            x = a.x;
            y = a.y - (a.y - b.y) / 2;
        } else if (Math.abs(a.x - b.x) == min / 2 && a.y == b.y) {//在同一条直线
            x = a.x - (a.x - b.x) / 2;
            y = a.y;
        } else if (Math.abs(a.x - b.x) == min / 2 && Math.abs(a.y - b.y) == min / 2) {//在同一条斜线上
            x = a.x - (a.x - b.x) / 2;
            y = a.y - (a.y - b.y) / 2;
        }
        p = choosePoints(x, y);
        Log.i(TAG, "X:" + x + ";;;;;Y:" + y);
        if (null != p) {
            p.state = Point.PRESSEDSTATE;
            mChoosePoints.add(len - 1, p);
            mChoosePassword.add(len - 1, p.index);
        }
    }

在这里,我是通过计算坐标的之间的差进行的,比较蠢。

ACTION_UP的处理就比较简单了,如果不是密码模式,就检测已经选中点的数量,如果超过范围用正常的方式展示,如果在范围之内,使用错误的方式展示。

最后就是调用postInvalidate()方法进行重绘。

恩,大体应该就是这么多。

最后说两句

虽然,整个小控件完成了,但是在编写这的控件中发现了很多问题,

  1. 命名的问题,命名感觉乱七八糟的。
  2. 思考问题的逻辑还是存在一些问题,逻辑不严谨,对于空判断等一些可能会造成异常的条件,没有进行控制。

以后好好努力吧。

整个项目我放在了github上,有兴趣的可以看看代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值