前言:
很多金融和几大商业银行的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,欢迎评论和交流!