很多金融和几大商业银行的APP,都使用了九宫格图形密码锁来增强资金账户的安全。我也是金融公司的一员,在空余的时候,写下这个view,可以说是明智之举。
这样一个逻辑差不多可以满足基本的需求了。接下来就看代码咯。
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; }这个View就是在绘制玩手势后的一个简单显示绘制的点的位置。
这个就比较简单了,很多都是 copy NineSquaredView的代码,就不细说了。
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,欢迎评论和交流!