Android自定义控件实现手势密码
GPT4.0+Midjourney绘画+国内大模型 会员永久免费使用!
【 如果你想靠AI翻身,你先需要一个靠谱的工具! 】
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类的源代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | 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的源码,首先贴源码,然后再针对性讲解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | 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个状态,分别是:
1 2 3 4 5 6 7 8 9 10 11 12 | //当前没有保存密码 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个状态:有密码、无密码。
1 2 3 4 5 6 7 8 9 10 11 | public void initStatus(){ if (TextUtils.isEmpty(Util.getPwd(getContext()))){ status = STATUS_NO_PWD; } else { status = STATUS_SAVE_PWD; } } public int getCurrentStatus(){ return status; } |
然后就是通过外界的设置初始化一些参数(若不调用initParam方法,则采用默认值):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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值,计算出小球的大小,然后判断是否是初始化小球,还是更新小球。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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中计算出来的
1 2 3 4 5 6 | @Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec){ width = MeasureSpec.getSize(widthMeasureSpec); height = width - getPaddingLeft() - getPaddingRight() + getPaddingTop() + getPaddingBottom(); setMeasuredDimension(width, height); } |
然后就是绘制方法了,因为我们的高度解耦性,本应该非常复杂的onDraw方法,却如此简单。就只绘制了小球和路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @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); } } |
控件是需要和外界进行交互的,我喜欢的方法就是自定义监听器,然后接口回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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事件代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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事件主要做三件事情:变更小球的状态、添加到路径集合、对路径覆盖的未点亮小球进行点亮。代码有详细注释就不过多讲解了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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事件,这里有太多复杂的状态转换,我估计文字讲解是描述不清的,大家还是看源代码吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | 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 ; |
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
微信公众号搜索 “ 脚本之家 ” ,选择关注
程序猿的那些事、送书等活动等着你
相关文章
Flutter交互并使用小工具管理其状态widget的state详解
这篇文章主要为大家介绍了Flutter交互并使用小工具管理其状态widget的state详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-12-12android仿知乎ScrollView滚动改变标题栏透明度
这篇文章主要为大家详细介绍了android仿知乎ScrollView滚动改变标题栏透明度,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2018-06-06Android 使用FragmentTabhost代替Tabhost
这篇文章主要介绍了Android 使用FragmentTabhost代替Tabhost的相关资料,需要的朋友可以参考下2017-05-05android中图片的三级缓存cache策略(内存/文件/网络)
实现图片缓存也不难,需要有相应的cache策略。这里我采用 内存-文件-网络 三层cache机制,其中内存缓存包括强引用缓存和软引用缓存(SoftReference),其实网络不算cache,这里姑且也把它划到缓存的层次结构中2013-06-06
最新评论