Android自定义控件实现手势密码
android手势解锁密码效果图
首先呢想写这个手势密码的想法呢,完全是凭空而来的,然后笔者就花了一天时间弄出来了。本以为这个东西很简单,实际上手的时候发现,还有很多逻辑需要处理,稍不注意就容易乱套。写个ui效果图大约只花了3个小时,但是处理逻辑就处理了2个小时!废话不多说,下面开始讲解。
楼主呢,自己比较自定义控件,什么东西都掌握在自己的手里感觉那是相当不错(对于赶工期的小伙瓣儿们还是别手贱了,非常容易掉坑),一有了这个目标,我就开始构思实现方式。
1、整个自定义控件是继承view还是surfaceview呢?我的经验告诉我:需要一直不断绘制的最好继承surfaceview,而需要频繁与用户交互的最好就继承view。(求大神来打脸)
2、为了实现控件的屏幕适配性,当然必须重写onmeasure方法,然后在ondraw方法中进行绘制。
3、面向对象性:这个控件其实由两个对象组成:1、9个圆球;2、圆球之间的连线。
4、仔细观察圆球的特征:普通状态是白色、touch状态是蓝色、错误状态是红色、整体分为外围空心圆和内实心圆、所代表的位置信息(密码值)
5、仔细观察连线的特征:普通状态为蓝色、错误状态为红色、始终连接两个圆的中心、跟随手指移动而拓展连线、连线之间未点亮的圆球也要点亮。
6、通过外露参数来设置圆球的颜色、大小等等
7、通过上面的分析,真个控件可模块化为三个任务:onmeasure计算控件宽高以及小球半径、ondraw绘制小球与连线、ontouchevent控制绘制变化。
我把整个源码分为三个类文件:lockview、circle、util,其中lockview代表整个控件,circle代表小圆球、util封装工具方法(path因为太简单就没封装,若有代码洁癖请自行封装),下面展示util类的源代码。
public class util{ private static final string sp_name = "lockview"; private static final string sp_key = "password"; public static void savepwd(context mcontext ,list<integer> password){ sharedpreferences sp = mcontext.getsharedpreferences(sp_name, context.mode_private); sp.edit().putstring(sp_key, listtostring(password)).commit(); } public static string getpwd(context mcontext){ sharedpreferences sp = mcontext.getsharedpreferences(sp_name, context.mode_private); return sp.getstring(sp_key, ""); } public static void clearpwd(context mcontext){ sharedpreferences sp = mcontext.getsharedpreferences(sp_name, context.mode_private); sp.edit().remove(sp_key).commit(); } public static string listtostring(list<integer> lists){ stringbuffer sb = new stringbuffer(); for(int i = 0; i < lists.size(); i++){ sb.append(lists.get(i)); } return sb.tostring(); } public static list<integer> stringtolist(string string){ list<integer> lists = new arraylist<>(); for(int i = 0; i < string.length(); i++){ lists.add(integer.parseint(string.charat(i) + "")); } return lists; } }
这个工具方法其实很简单,就是对sharedpreferences的一个读写,还有就是list与string类型的互相转换。这里就不描述了。下面展示circle的源码
public class circle{ //默认值 public static final int default_color = color.white; public static final int default_bound = 5; public static final int default_center_bound = 15; //状态值 public static final int status_default = 0; public static final int status_touch = 1; public static final int status_success = 2; public static final int status_failed = 3; //圆形的中点x、y坐标 private int centerx; private int centery; //圆形的颜色值 private int colordefault = default_color; private int colorsuccess; private int colorfailed; //圆形的宽度 private int bound = default_bound; //中心的宽度 private int centerbound = default_center_bound; //圆形的半径 private int radius; //圆形的状态 private int status = status_default; //圆形的位置 private int position; public circle(int centerx, int centery, int colorsuccess, int colorfailed, int radius, int position){ super(); this.centerx = centerx; this.centery = centery; this.colorsuccess = colorsuccess; this.colorfailed = colorfailed; this.radius = radius; this.position = position; } public circle(int centerx, int centery, int colordefault, int colorsuccess, int colorfailed, int bound, int centerbound, int radius, int status, int position){ super(); this.centerx = centerx; this.centery = centery; this.colordefault = colordefault; this.colorsuccess = colorsuccess; this.colorfailed = colorfailed; this.bound = bound; this.centerbound = centerbound; this.radius = radius; this.status = status; this.position = position; } public int getcenterx(){ return centerx; } public void setcenterx(int centerx){ this.centerx = centerx; } public int getcentery(){ return centery; } public void setcentery(int centery){ this.centery = centery; } public int getcolordefault(){ return colordefault; } public void setcolordefault(int colordefault){ this.colordefault = colordefault; } public int getcolorsuccess(){ return colorsuccess; } public void setcolorsuccess(int colorsuccess){ this.colorsuccess = colorsuccess; } public int getcolorfailed(){ return colorfailed; } public void setcolorfailed(int colorfailed){ this.colorfailed = colorfailed; } public int getbound(){ return bound; } public void setbound(int bound){ this.bound = bound; } public int getcenterbound(){ return centerbound; } public void setcenterbound(int centerbound){ this.centerbound = centerbound; } public int getradius(){ return radius; } public void setradius(int radius){ this.radius = radius; } public int getstatus(){ return status; } public void setstatus(int status){ this.status = status; } public int getposition(){ return position; } public void setposition(int position){ this.position = position; } /** * @description:改变圆球当前状态 */ public void changestatus(int status){ this.status = status; } /** * @description:绘制这个圆形 */ public void draw(canvas canvas ,paint paint){ switch(status){ case status_default: paint.setcolor(colordefault); break; case status_touch: case status_success: paint.setcolor(colorsuccess); break; case status_failed: paint.setcolor(colorfailed); break; default: paint.setcolor(colordefault); break; } paint.setstyle(paint.style.fill); //绘制中心实心圆 canvas.drawcircle(centerx, centery, centerbound, paint); //绘制空心圆 paint.setstyle(paint.style.stroke); paint.setstrokewidth(bound); canvas.drawcircle(centerx, centery, radius, paint); } }
这个circle其实也非常简单。上面定义的成员变量一眼便明,并且有注释。重点在最后的draw方法,首先呢根据当前圆球的不同状态设置不同的颜色值,然后绘制中心的实心圆,再绘制外围的空心圆。所有的参数要么是外界传递,要么是默认值。(ps:面向对象真的非常有用,解耦良好的代码写起来也舒服看起来也舒服)。
最后的重点来了,lockview的源码,首先贴源码,然后再针对性讲解。
public class lockview extends view{ private static final int count_per_raw = 3; private static final int duration = 1500; private static final int min_pwd_number = 6; //@fields status_no_pwd : 当前没有保存密码 public static final int status_no_pwd = 0; //@fields status_retry_pwd : 需要再输入一次密码 public static final int status_retry_pwd = 1; //@fields status_save_pwd : 成功保存密码 public static final int status_save_pwd = 2; //@fields status_success_pwd : 成功验证密码 public static final int status_success_pwd = 3; //@fields status_failed_pwd : 验证密码失败 public static final int status_failed_pwd = 4; //@fields status_error : 输入密码长度不够 public static final int status_error = 5; private int width; private int height; private int padding = 0; private int colorsuccess = color.blue; private int colorfailed = color.red; private int minpwdnumber = min_pwd_number; private list<circle> circles = new arraylist<>(); private paint mpaint = new paint(paint.anti_alias_flag); private path mpath = new path(); private path backupspath = new path(); private list<integer> result = new arraylist<>(); private int status = status_no_pwd; private onlocklistener listener; private handler handler = new handler(); public lockview(context context, attributeset attrs, int defstyle){ super(context, attrs, defstyle); initstatus(); } public lockview(context context, attributeset attrs){ super(context, attrs); initstatus(); } public lockview(context context){ super(context); initstatus(); } /** * @description:初始化当前密码的状态 */ public void initstatus(){ if(textutils.isempty(util.getpwd(getcontext()))){ status = status_no_pwd; }else{ status = status_save_pwd; } } public int getcurrentstatus(){ return status; } /** * @description:初始化参数,若不调用则使用默认值 * @param padding 圆球之间的间距 * @param colorsuccess 密码正确时圆球的颜色 * @param colorfailed 密码错误时圆球的颜色 * @return lockview */ public lockview initparam(int padding ,int colorsuccess ,int colorfailed ,int minpwdnumber){ this.padding = padding; this.colorsuccess = colorsuccess; this.colorfailed = colorfailed; this.minpwdnumber = minpwdnumber; init(); return this; } /** * @description:若第一次调用则创建圆球,否则更新圆球 */ private void init(){ int circleradius = (width - (count_per_raw + 1) * padding) / count_per_raw /2; if(circles.size() == 0){ for(int i = 0; i < count_per_raw * count_per_raw; i++){ createcircles(circleradius, i); } }else{ for(int i = 0; i < count_per_raw * count_per_raw; i++){ updatecircles(circles.get(i), circleradius); } } } private void createcircles(int radius, int position){ int centerx = (position % 3 + 1) * padding + (position % 3 * 2 + 1) * radius; int centery = (position / 3 + 1) * padding + (position / 3 * 2 + 1) * radius; circle circle = new circle(centerx, centery, colorsuccess, colorfailed, radius, position); circles.add(circle); } private void updatecircles(circle circle ,int radius){ int centerx = (circle.getposition() % 3 + 1) * padding + (circle.getposition() % 3 * 2 + 1) * radius; int centery = (circle.getposition() / 3 + 1) * padding + (circle.getposition() / 3 * 2 + 1) * radius; circle.setcenterx(centerx); circle.setcentery(centery); circle.setradius(radius); circle.setcolorsuccess(colorsuccess); circle.setcolorfailed(colorfailed); } @override protected void ondraw(canvas canvas){ init(); //绘制圆 for(int i = 0; i < circles.size() ;i++){ circles.get(i).draw(canvas, mpaint); } if(result.size() != 0){ //绘制path circle temp = circles.get(result.get(0)); mpaint.setcolor(temp.getstatus() == circle.status_failed ? colorfailed : colorsuccess); mpaint.setstrokewidth(circle.default_center_bound); canvas.drawpath(mpath, mpaint); } } @override public boolean ontouchevent(motionevent event){ switch(event.getaction()){ case motionevent.action_down: backupspath.reset(); for(int i = 0; i < circles.size() ;i++){ circle circle = circles.get(i); if(event.getx() >= circle.getcenterx() - circle.getradius() && event.getx() <= circle.getcenterx() + circle.getradius() && event.gety() >= circle.getcentery() - circle.getradius() && event.gety() <= circle.getcentery() + circle.getradius()){ circle.setstatus(circle.status_touch); //将这个点放入path backupspath.moveto(circle.getcenterx(), circle.getcentery()); //放入结果 result.add(circle.getposition()); break; } } invalidate(); return true; case motionevent.action_move: for(int i = 0; i < circles.size() ;i++){ circle circle = circles.get(i); if(event.getx() >= circle.getcenterx() - circle.getradius() && event.getx() <= circle.getcenterx() + circle.getradius() && event.gety() >= circle.getcentery() - circle.getradius() && event.gety() <= circle.getcentery() + circle.getradius()){ if(!result.contains(circle.getposition())){ circle.setstatus(circle.status_touch); //首先判断是否连线中间也有满足条件的圆 circle lastcircle = circles.get(result.get(result.size() - 1)); int cx = (lastcircle.getcenterx() + circle.getcenterx()) / 2; int cy = (lastcircle.getcentery() + circle.getcentery()) / 2; for(int j = 0; j < circles.size(); j++){ circle tempcircle = circles.get(j); if(cx >= tempcircle.getcenterx() - tempcircle.getradius() && cx <= tempcircle.getcenterx() + tempcircle.getradius() && cy >= tempcircle.getcentery() - tempcircle.getradius() && cy <= tempcircle.getcentery() + tempcircle.getradius()){ //处理满足条件的圆 backupspath.lineto(tempcircle.getcenterx(), tempcircle.getcentery()); //放入结果 tempcircle.setstatus(circle.status_touch); result.add(tempcircle.getposition()); } } //处理现在的圆 backupspath.lineto(circle.getcenterx(), circle.getcentery()); //放入结果 circle.setstatus(circle.status_touch); result.add(circle.getposition()); break; } } } mpath.reset(); mpath.addpath(backupspath); mpath.lineto(event.getx(), event.gety()); invalidate(); break; case motionevent.action_up: mpath.reset(); mpath.addpath(backupspath); invalidate(); if(result.size() < minpwdnumber){ if(listener != null){ listener.onerror(); } if(status == status_retry_pwd){ util.clearpwd(getcontext()); } status = status_error; for(int i = 0; i < result.size(); i++){ circles.get(result.get(i)).setstatus(circle.status_failed); } }else{ if(status == status_no_pwd){ //当前没有密码 //保存密码,重新录入 util.savepwd(getcontext(), result); status = status_retry_pwd; if(listener != null){ listener.ontypeinonce(util.listtostring(result)); } }else if(status == status_retry_pwd){ //需要重新绘制密码 //判断两次输入是否相等 if(util.getpwd(getcontext()).equals(util.listtostring(result))){ status = status_save_pwd; if(listener != null){ listener.ontypeintwice(util.listtostring(result), true); } for(int i = 0; i < result.size(); i++){ circles.get(result.get(i)).setstatus(circle.status_success); } }else{ status = status_no_pwd; util.clearpwd(getcontext()); if(listener != null){ listener.ontypeintwice(util.listtostring(result), false); } for(int i = 0; i < result.size(); i++){ circles.get(result.get(i)).setstatus(circle.status_failed); } } }else if(status == status_save_pwd){ //验证密码 //判断密码是否正确 if(util.getpwd(getcontext()).equals(util.listtostring(result))){ status = status_success_pwd; if(listener != null){ listener.onunlock(util.listtostring(result), true); } for(int i = 0; i < result.size(); i++){ circles.get(result.get(i)).setstatus(circle.status_success); } }else{ status = status_failed_pwd; if(listener != null){ listener.onunlock(util.listtostring(result), false); } for(int i = 0; i < result.size(); i++){ circles.get(result.get(i)).setstatus(circle.status_failed); } } } } invalidate(); handler.postdelayed(new runnable(){ @override public void run(){ result.clear(); mpath.reset(); backupspath.reset(); // initstatus(); // 重置下状态 if(status == status_success_pwd || status == status_failed_pwd){ status = status_save_pwd; }else if(status == status_error){ initstatus(); } for(int i = 0; i < circles.size(); i++){ circles.get(i).setstatus(circle.status_default); } invalidate(); } }, duration); break; default: break; } return super.ontouchevent(event); } @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec){ width = measurespec.getsize(widthmeasurespec); height = width - getpaddingleft() - getpaddingright() + getpaddingtop() + getpaddingbottom(); setmeasureddimension(width, height); } public void setonlocklistener(onlocklistener listener){ this.listener = listener; } public interface onlocklistener{ /** * @description:没有密码时,第一次录入密码触发器 */ void ontypeinonce(string input); /** * @description:已经录入第一次密码,录入第二次密码触发器 */ void ontypeintwice(string input ,boolean issuccess); /** * @description:验证密码触发器 */ void onunlock(string input ,boolean issuccess); /** * @description:密码长度不够 */ void onerror(); } }
好了,逐次讲解。
首先是对status的初始化,其实在static域我已经申明了6个状态,分别是:
//当前没有保存密码 public static final int status_no_pwd = 0; //需要再输入一次密码 public static final int status_retry_pwd = 1; //成功保存密码 public static final int status_save_pwd = 2; //成功验证密码 public static final int status_success_pwd = 3; //验证密码失败 public static final int status_failed_pwd = 4; //输入密码长度不够 public static final int status_error = 5;
在刚初始化的时候,就初始化当前的状态,初始化状态就只有2个状态:有密码、无密码。
public void initstatus(){ if(textutils.isempty(util.getpwd(getcontext()))){ status = status_no_pwd; }else{ status = status_save_pwd; } } public int getcurrentstatus(){ return status; }
然后就是通过外界的设置初始化一些参数(若不调用initparam方法,则采用默认值):
public lockview initparam(int padding ,int colorsuccess ,int colorfailed ,int minpwdnumber){ this.padding = padding; this.colorsuccess = colorsuccess; this.colorfailed = colorfailed; this.minpwdnumber = minpwdnumber; init(); return this; } /** * @description:若第一次调用则创建圆球,否则更新圆球 */ private void init(){ int circleradius = (width - (count_per_raw + 1) * padding) / count_per_raw /2; if(circles.size() == 0){ for(int i = 0; i < count_per_raw * count_per_raw; i++){ createcircles(circleradius, i); } }else{ for(int i = 0; i < count_per_raw * count_per_raw; i++){ updatecircles(circles.get(i), circleradius); } } }
上述代码主要根据设置的padding值,计算出小球的大小,然后判断是否是初始化小球,还是更新小球。
private void createcircles(int radius, int position){ int centerx = (position % 3 + 1) * padding + (position % 3 * 2 + 1) * radius; int centery = (position / 3 + 1) * padding + (position / 3 * 2 + 1) * radius; circle circle = new circle(centerx, centery, colorsuccess, colorfailed, radius, position); circles.add(circle); } private void updatecircles(circle circle ,int radius){ int centerx = (circle.getposition() % 3 + 1) * padding + (circle.getposition() % 3 * 2 + 1) * radius; int centery = (circle.getposition() / 3 + 1) * padding + (circle.getposition() / 3 * 2 + 1) * radius; circle.setcenterx(centerx); circle.setcentery(centery); circle.setradius(radius); circle.setcolorsuccess(colorsuccess); circle.setcolorfailed(colorfailed); }
别忘了上面的方法依赖一个width值,这个值是在onmeasure中计算出来的
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec){ width = measurespec.getsize(widthmeasurespec); height = width - getpaddingleft() - getpaddingright() + getpaddingtop() + getpaddingbottom(); setmeasureddimension(width, height); }
然后就是绘制方法了,因为我们的高度解耦性,本应该非常复杂的ondraw方法,却如此简单。就只绘制了小球和路径。
@override protected void ondraw(canvas canvas){ init(); //绘制圆 for(int i = 0; i < circles.size() ;i++){ circles.get(i).draw(canvas, mpaint); } if(result.size() != 0){ //绘制path circle temp = circles.get(result.get(0)); mpaint.setcolor(temp.getstatus() == circle.status_failed ? colorfailed : colorsuccess); mpaint.setstrokewidth(circle.default_center_bound); canvas.drawpath(mpath, mpaint); } }
控件是需要和外界进行交互的,我喜欢的方法就是自定义监听器,然后接口回调。
public void setonlocklistener(onlocklistener listener){ this.listener = listener; } public interface onlocklistener{ /** * @description:没有密码时,第一次录入密码触发器 */ void ontypeinonce(string input); /** * @description:已经录入第一次密码,录入第二次密码触发器 */ void ontypeintwice(string input ,boolean issuccess); /** * @description:验证密码触发器 */ void onunlock(string input ,boolean issuccess); /** * @description:密码长度不够 */ void onerror(); }
最后最最最重要的一个部分来了,ontouchevent方法,这个方法其实也可以分为三个部分讲解:down事件、move事件和up事件。首先贴出down事件代码
case motionevent.action_down: backupspath.reset(); for(int i = 0; i < circles.size() ;i++){ circle circle = circles.get(i); if(event.getx() >= circle.getcenterx() - circle.getradius() && event.getx() <= circle.getcenterx() + circle.getradius() && event.gety() >= circle.getcentery() - circle.getradius() && event.gety() <= circle.getcentery() + circle.getradius()){ circle.setstatus(circle.status_touch); //将这个点放入path backupspath.moveto(circle.getcenterx(), circle.getcentery()); //放入结果 result.add(circle.getposition()); break; } } invalidate(); return true;
也就是对按下的x、y坐标进行判断,是否属于我们的小球范围内,若属于,则放入路径集合、更改状态、加入密码结果集。这里别忘了return true,大家都知道吧。
然后是move事件,move事件主要做三件事情:变更小球的状态、添加到路径集合、对路径覆盖的未点亮小球进行点亮。代码有详细注释就不过多讲解了。
case motionevent.action_move: for(int i = 0; i < circles.size() ;i++){ circle circle = circles.get(i); if(event.getx() >= circle.getcenterx() - circle.getradius() && event.getx() <= circle.getcenterx() + circle.getradius() && event.gety() >= circle.getcentery() - circle.getradius() && event.gety() <= circle.getcentery() + circle.getradius()){ if(!result.contains(circle.getposition())){ circle.setstatus(circle.status_touch); //首先判断是否连线中间也有满足条件的圆 circle lastcircle = circles.get(result.get(result.size() - 1)); int cx = (lastcircle.getcenterx() + circle.getcenterx()) / 2; int cy = (lastcircle.getcentery() + circle.getcentery()) / 2; for(int j = 0; j < circles.size(); j++){ circle tempcircle = circles.get(j); if(cx >= tempcircle.getcenterx() - tempcircle.getradius() && cx <= tempcircle.getcenterx() + tempcircle.getradius() && cy >= tempcircle.getcentery() - tempcircle.getradius() && cy <= tempcircle.getcentery() + tempcircle.getradius()){ //处理满足条件的圆 backupspath.lineto(tempcircle.getcenterx(), tempcircle.getcentery()); //放入结果 tempcircle.setstatus(circle.status_touch); result.add(tempcircle.getposition()); } } //处理现在的圆 backupspath.lineto(circle.getcenterx(), circle.getcentery()); //放入结果 circle.setstatus(circle.status_touch); result.add(circle.getposition()); break; } } } mpath.reset(); mpath.addpath(backupspath); mpath.lineto(event.getx(), event.gety()); invalidate(); break;
这里我用了两个path对象,backupspath用于只存放小球的中点坐标,mpath不仅要存储小球的中点坐标,还要存储当前手指触碰坐标,为了实现连线跟随手指运动的效果。
最后是up事件,这里有太多复杂的状态转换,我估计文字讲解是描述不清的,大家还是看源代码吧。
case motionevent.action_up: mpath.reset(); mpath.addpath(backupspath); invalidate(); if(result.size() < minpwdnumber){ if(listener != null){ listener.onerror(); } if(status == status_retry_pwd){ util.clearpwd(getcontext()); } status = status_error; for(int i = 0; i < result.size(); i++){ circles.get(result.get(i)).setstatus(circle.status_failed); } }else{ if(status == status_no_pwd){ //当前没有密码 //保存密码,重新录入 util.savepwd(getcontext(), result); status = status_retry_pwd; if(listener != null){ listener.ontypeinonce(util.listtostring(result)); } }else if(status == status_retry_pwd){ //需要重新绘制密码 //判断两次输入是否相等 if(util.getpwd(getcontext()).equals(util.listtostring(result))){ status = status_save_pwd; if(listener != null){ listener.ontypeintwice(util.listtostring(result), true); } for(int i = 0; i < result.size(); i++){ circles.get(result.get(i)).setstatus(circle.status_success); } }else{ status = status_no_pwd; util.clearpwd(getcontext()); if(listener != null){ listener.ontypeintwice(util.listtostring(result), false); } for(int i = 0; i < result.size(); i++){ circles.get(result.get(i)).setstatus(circle.status_failed); } } }else if(status == status_save_pwd){ //验证密码 //判断密码是否正确 if(util.getpwd(getcontext()).equals(util.listtostring(result))){ status = status_success_pwd; if(listener != null){ listener.onunlock(util.listtostring(result), true); } for(int i = 0; i < result.size(); i++){ circles.get(result.get(i)).setstatus(circle.status_success); } }else{ status = status_failed_pwd; if(listener != null){ listener.onunlock(util.listtostring(result), false); } for(int i = 0; i < result.size(); i++){ circles.get(result.get(i)).setstatus(circle.status_failed); } } } } invalidate(); handler.postdelayed(new runnable(){ @override public void run(){ result.clear(); mpath.reset(); backupspath.reset(); // initstatus(); // 重置下状态 if(status == status_success_pwd || status == status_failed_pwd){ status = status_save_pwd; }else if(status == status_error){ initstatus(); } for(int i = 0; i < circles.size(); i++){ circles.get(i).setstatus(circle.status_default); } invalidate(); } }, duration); break;
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: Android 常见的四种对话框实例讲解
下一篇: Android数据加密之Des加密