自定义View之--九宫格图形密码锁

前言:

很多金融和几大商业银行的APP,都使用了九宫格图形密码锁来增强资金账户的安全。我也是金融公司的一员,在空余的时候,写下这个view,可以说是明智之举。

效果预览

这里写图片描述

这样一个逻辑差不多可以满足基本的需求了。接下来就看代码咯。

NineSquareView的成长

1、重写构造方法和初始化属性

    private Paint pointPaint;  //画点的画笔
    private Paint linePaint; // 画线的画笔
    private Path path;     //路径
    private static int SQUAREWIDRH = 300; //默认正方形的边长
    private float mSquarewidth = SQUAREWIDRH; //每个正方形的边长 9个
    private float x, y; //手指在滑动的时候那个点的坐标
    private float  startX, startY; //手指首次接触View的那个点的坐标
    private LinkedHashMap<String,Point> points = new LinkedHashMap<>(); //存放手指连接的点
    private OnFinishGestureListener finishGestureListener ; //当手指抬起时,触发的监听

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

    public NineSquareView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NineSquareView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        linePaint = new Paint();
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setColor(Color.CYAN);
        linePaint.setStrokeWidth(5);
        linePaint.setAntiAlias(true);
        linePaint.setStrokeCap(Paint.Cap.ROUND);
        pointPaint = new Paint();
        pointPaint.setStyle(Paint.Style.FILL);
        pointPaint.setColor(Color.parseColor("#cbd0de"));
        pointPaint.setStrokeWidth(40);
        pointPaint.setAntiAlias(true);
        pointPaint.setStrokeCap(Paint.Cap.ROUND);
        path =new Path();
    }
   public interface OnFinishGestureListener {
       void onfinish(LinkedHashMap<String,Point> points);

    }

2、重写onMeasure();

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wideSize = MeasureSpec.getSize(widthMeasureSpec);
        int wideMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width, height;
        if (wideMode == MeasureSpec.EXACTLY) { //精确值 或matchParent
            width = wideSize;
        } else {
            width = (int) (mSquarewidth * 3 + getPaddingLeft() + getPaddingRight());
            if (wideMode == MeasureSpec.AT_MOST) {
                width = Math.min(width, wideSize);
            }
        }
        if (heightMode == MeasureSpec.EXACTLY) { //精确值 或matchParent
            height = heightSize;
        } else {
            height = (int) (mSquarewidth * 3 + getPaddingTop() + getPaddingBottom());
            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(height, heightSize);
            }
        }
        setMeasuredDimension(width, height);
        mSquarewidth = (int) (Math.min(width - getPaddingLeft() - getPaddingRight(),
                height - getPaddingTop() - getPaddingBottom()) * 1.0f / 3);

    }

mSquarewidth始终是View的三分之一的宽度。对OnMeasure()方法还不是很懂的。可以去看看鸿神写的博客Android 自定义View (二) 进阶

3、重写onTouchEvent();

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = ev.getX();
                startY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                x = ev.getX();
                y = ev.getY();
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                x = 0;
                y = 0;
                startX = 0;
                startY = 0;
                finishGestureListener.onfinish(points);
                points.clear();
                invalidate();
                break;
        }
        return true;
    }

在手指离开屏幕的时候,就是绘制完成的时候,所有数据清零。并触发finishGestureListener,去处理当前用户连接的points.
4.重写onDraw();
最重要的,最精彩的部分来了。首先我们得把九个灰点画出来。来个双层for循环就搞定。

for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                pointPaint.setColor(Color.parseColor("#cbd0de"));
                canvas.drawPoint(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j),pointPaint);
            }
        }

每个灰色的点都画在正方形的中央。可接下来有个问题就要思考了,我们的手指去绘制的时候,要判断手指触碰的点是不是正好是那些个灰点。判断两个坐标是否相等?NONONO,我们画的点比我们的手指要细些。手指要精确的触碰到那个灰点,估计有点困难。照这样下去,你的app早就被用户卸载了。
我们可以给一个范围,这个范围是用户触碰的点离最近的那个灰点的距离。比如mSquarewidth * 0.3f,如果手指触摸在这个范围内,就说明用户想要绘制这个点。这个范围不能超过mSquarewidth * 0.5f,然后,我们把这个点加入到集合中。

  for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (Math.abs(startX - mSquarewidth * (0.5f + i)) < mSquarewidth * 0.3f &&
                        Math.abs(startY - mSquarewidth * (0.5f + j)) < mSquarewidth * 0.3f) {
                    path.moveTo(mSquarewidth * (0.5f + i), mSquarewidth * (0.5f + j));
                    path.lineTo(x, y);
                    canvas.drawPath(path,linePaint);
                    path.reset();
                    Point point =new Point(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j));
                    points.put(i+":"+j,point);
                    System.out.println(points.size());
                    System.out.println(i+"//"+j);
                }
            }
        }

这样写完后,运行写代码。结果就是,只能加入手指点下去的第一个点,想连接下一个点,怎么办?继续思考,写代码。刚才,我们已经连接到了第一个点,想要连接到第二个点,我们必须滑动我们的手指,滑动的时候,坐标变为了x,y.而且时时刻刻在变动。再来一次范围判断,是不是就可以连接到第二个点了?答案是正确的!

for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (Math.abs(x - mSquarewidth * (0.5f + i)) < mSquarewidth * 0.3f &&
                        Math.abs(y - mSquarewidth * (0.5f + j)) <mSquarewidth * 0.3f
                        ) {
                    Iterator<Point> iterator2 = collection.iterator();
                    while(iterator2.hasNext()){
                        Point point = iterator2.next();
                       if(mSquarewidth * (0.5f + i)==point.getX() && mSquarewidth * (0.5f + j)==point.getY()){
                              return;
                       }
                    }
                    startX = mSquarewidth * (0.5f + i);
                    startY = mSquarewidth * (0.5f + j);

                }
            }
        }

但要排除下,我们已经连接过的点。并把这连接好的第二个点设为起始点。这样就可以循环的连接点了。在一开始的效果预览中可以看到,连接过的点,会变一种颜色,而且还会有一个小圆环,点与点之间会有一根线连接着,不会消失。这也好办。

 Collection<Point> collection = points.values();
        Iterator<Point> iterator = collection.iterator();
        if(iterator.hasNext()){
            Point point = iterator.next();
            drawCyanPoint(canvas,point);
            System.out.println("moveTo:"+point.getX()+"===="+point.getY());
            path.moveTo(point.getX(),point.getY());
        }
        while (iterator.hasNext()) {
            Point point = iterator.next();
            drawCyanPoint(canvas,point);
            System.out.println("lineTo:"+point.getX()+"===="+point.getY());
            path.lineTo(point.getX(),point.getY());
        }
        canvas.drawPath(path,linePaint);
        path.reset();

在画了灰点后,可以把map中的points连接起来。改变画笔的颜色,画上圆圈,这个圆圈的半径最好是你设置的那个范围的大小。我的是mSquarewidth * 0.3f。

 //绘制手指划到的那个点,点外加上一层圈。
    public void drawCyanPoint(Canvas canvas, Point point){
        String s =getKey(point);
        String [] strings =  s.split(":");
        int i= Integer.parseInt(strings[0]);
        int j=Integer.parseInt(strings[1]);
        pointPaint.setColor(Color.CYAN);
        canvas.drawPoint(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j),pointPaint);
        canvas.drawCircle(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j),mSquarewidth * 0.3f,linePaint);
    }

    //根据value取key值
    public  String getKey(Point value)
    {
        String key = "";
        Set<Map.Entry<String, Point>> set = points.entrySet();
        for(Map.Entry<String, Point> entry : set){
            if(entry.getValue().equals(value)){
                key = entry.getKey();
                break;
            }
        }
        return key;
    }

NinePointView的成长

这个View就是在绘制玩手势后的一个简单显示绘制的点的位置。

这里写图片描述

这个就比较简单了,很多都是 copy NineSquaredView的代码,就不细说了。

PswActivity的成长。

Activity中的就是逻辑和UI了。PswActivity包含设置密码锁和解锁并跳转到其他界面。大致逻辑我们都懂的,就不细说了。唯一要说的就是比较两次设置的密码是否一致,以及设置密码与解锁密码是否一致。我们要比较两次的密码是否一致,其实就是比较两次绘制时的绘制点的个数,位置是否一致。

public boolean isEquals(LinkedHashMap<String, Point> pointsOne,LinkedHashMap<String, Point> pointsTwo) {
        Iterator<String> iterator = pointsOne.keySet().iterator();
        Iterator<String> iterator2 = pointsTwo.keySet().iterator();
        if (pointsOne.size() != pointsTwo.size()) {
            return false;
        }
        while (iterator.hasNext()) {
            String s = iterator.next();
            String s2 = iterator2.next();
            if (!s.equals(s2)) {
                return false;
            }
        }
        return true;
    }

因为LinkedHashMap是有序的,所以才能这样一个一个对应的去比较。我们设置密码后,密码是需要存放在本地的,SharedPreferences来帮忙了。等到下一次打开APP的时候,才能与解锁密码作比较。可寻遍了SharedPreferences中的put相关方法,就是没有能把LinkedHashMap放进去的。刚还思考着呢,Stream来帮忙了。通过写流和读流,这样操作更加安全。

public  String map2String(LinkedHashMap<String, Point> hashmap) {
        // 实例化一个ByteArrayOutputStream对象,用来装载压缩后的字节文件。
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        String sceneListString = null;
        // 然后将得到的字符数据装载到ObjectOutputStream
        ObjectOutputStream objectOutputStream = null;
        try {
            objectOutputStream = new ObjectOutputStream(
                    byteArrayOutputStream);
            // writeObject 方法负责写入特定类的对象的状态,以便相应的 readObject 方法可以还原它
            objectOutputStream.writeObject(hashmap);
            // 最后,用Base64.encode将字节文件转换成Base64编码保存在String中
            sceneListString = new String(Base64.encode(
                    byteArrayOutputStream.toByteArray(), Base64.DEFAULT),"utf8");
            // 关闭objectOutputStream
            objectOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sceneListString;
    }

    public LinkedHashMap<String, Point> getHashMap() {
        String liststr = preferences.getString(PREFERENCENAME, null);
        try {
            return string2Map(liststr);
        } catch (StreamCorruptedException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public  LinkedHashMap<String, Point> string2Map(
            String SceneListString) throws
            IOException, ClassNotFoundException {
        byte[] mobileBytes = Base64.decode(SceneListString.getBytes(),
                Base64.DEFAULT);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(
                mobileBytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(
                byteArrayInputStream);
        LinkedHashMap<String, Point> SceneList = (LinkedHashMap<String, Point>) objectInputStream
                .readObject();
        objectInputStream.close();
        return SceneList;
    }

所有代码链接:

https://github.com/Demidong/ClockView.git

That all,欢迎评论和交流!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值