• android-手势密码


    引子

    手势密码,移动开发中的常用功能点,看起来高大上,其实挺简单的。

    本文提供 我自定义的 手势密码控件布局,以及使用方法,首先附上github地址:https://github.com/18598925736/EazyGesturePwdLayoutDemo

    实际效果动态图

    设置手势密码:

    设置手势密码,当 前后两次的手势不一样时

     

    校验手势密码-当5次都错时:

    校验手势密码-当5次之内输入正确时

    重新设置手势(之前设置过,现在需要修改手势密码)

    源码解析

    首先说下开发思路:

    上面的图里面,我们主要看到了9个圆点,以及随着手势而产生的线条;

    9个圆点,其实就是 自定义的View,如果你运行demo,把手放上去的画,你会发现原点会出现圆环背景,这是在自定义的时候加上的功能,至于圆环的颜色宽度神马的,你开心的话自己就行了。

    至于线条,其实是 通过在一个自定义ViewGroup上重写onToucheEvent监测 down,move和up来绘制的,9个圆点是被放置(用的 addView)在这个自定义ViewGroup里面,排布的方式看看源码应该能明白;

    特别说明一下这里有个坑:

    在绘制线条的时候,我发现 我绘制出来的线条总是被9个圆点覆盖,经过多方查询,最终得出结论:这是ViewGroup的绘制机制导致的,它默认的绘制顺序,是先绘制 background,然后是自己,然后是子,最后是装饰;

    看起来很抽象是吧?

    看源码;

    最下方这个英语翻译过来,就是我刚才说的意思,由于后绘制的会覆盖先绘制的,所以,线条被子覆盖也是正常的。

    但是,这不是我想要的效果,问题是不是无解了呢?

    也不是,只是大路不通,要走小路了;

    还是看 View.java源码:

    发现,在绘制的第四步,DrawChildren中,调用的方法是dispatchDraw(canvas); 

    那我如果在绘制子之后,再画线,是不是可以让线条覆盖子。

    所以,我重写了这个方法,执行super.dispatchDraw()先保持原有逻辑,并且在执行我自己的绘制来画线;

     OK,坑 解释完毕。

    自定义控件的源码:

    业内人士应该没有什么看不懂的,毕竟我这个注释已经是详细得令人发指了(●´∀`●)....

    首先是那9个圆点:

      1 package com.example.gesture_password_study.gesture_pwd.custom;
      2 
      3 import android.content.Context;
      4 import android.content.res.TypedArray;
      5 import android.graphics.Canvas;
      6 import android.graphics.Paint;
      7 import android.support.annotation.Nullable;
      8 import android.util.AttributeSet;
      9 import android.view.View;
     10 
     11 import com.example.gesture_password_study.R;
     12 
     13 
     14 /**
     15  * 手势密码专用的圆形控件
     16  */
     17 public class GestureLockCircleView extends View {
     18 
     19     public GestureLockCircleView(Context context) {
     20         this(context, null);
     21     }
     22 
     23     public GestureLockCircleView(Context context, @Nullable AttributeSet attrs) {
     24         this(context, attrs, 0);
     25     }
     26 
     27     public GestureLockCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
     28         super(context, attrs, defStyleAttr);
     29         dealAttr(context, attrs);
     30         initPaint();
     31     }
     32 
     33     private void dealAttr(Context context, AttributeSet attrs) {
     34         TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.GestureLockCircleView);
     35 
     36         if (ta != null) {
     37             try {
     38                 circleFillColor = ta.getColor(R.styleable.GestureLockCircleView_gestureCircleFillColor, 0x00FE6665);
     39                 circleRadius = ta.getDimension(R.styleable.GestureLockCircleView_gestureCircleRadius, 0);
     40 
     41                 hasRoundBorder = ta.getBoolean(R.styleable.GestureLockCircleView_hasRoundBorder, false);
     42                 roundBorderColor = ta.getColor(R.styleable.GestureLockCircleView_roundBorderColor, 0x00FE6665);
     43                 roundBorderWidth = ta.getDimension(R.styleable.GestureLockCircleView_roundBorderWidth, 0);
     44             } catch (Exception e) {
     45 
     46             } finally {
     47                 ta.recycle();
     48             }
     49         }
     50     }
     51 
     52 
     53     private int minWidth = 50, minHeight = 50;
     54 
     55     /**
     56      * 重写onMeasure设定最小宽高
     57      *
     58      * @param widthMeasureSpec
     59      * @param heightMeasureSpec
     60      */
     61     @Override
     62     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     63         setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
     64         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
     65         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
     66         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
     67         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
     68 
     69         if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
     70             setMeasuredDimension(minWidth, minHeight);
     71         } else if (widthMode == MeasureSpec.AT_MOST) {
     72             setMeasuredDimension(minWidth, heightSize);
     73         } else if (heightMode == MeasureSpec.AT_MOST) {
     74             setMeasuredDimension(widthSize, minHeight);
     75         }
     76     }
     77 
     78     @Override
     79     protected void onDraw(Canvas canvas) {
     80         super.onDraw(canvas);
     81         int width = getWidth();
     82         int height = getHeight();
     83 
     84         float centerX = width / 2;
     85         float centerY = height / 2;
     86 
     87         if (hasRoundBorder) {
     88             canvas.drawCircle(centerX, centerY, roundBorderWidth, paint_border);
     89         }
     90         canvas.drawCircle(centerX, centerY, circleRadius, paint_inner);
     91 
     92     }
     93 
     94     private Paint paint_inner, paint_border;
     95 
     96 
     97     private boolean hasRoundBorder;
     98     private int roundBorderColor;
     99     private float roundBorderWidth;
    100 
    101     /**
    102      * 设置内圈的颜色和半径
    103      *
    104      * @param circleFillColor
    105      * @param circleRadius
    106      */
    107     public void setInnerCircle(int circleFillColor, float circleRadius) {
    108         this.circleFillColor = circleFillColor;
    109         this.circleRadius = circleRadius;
    110         initPaint();
    111         postInvalidate();
    112     }
    113 
    114     public void setBorderRound(boolean hasRoundBorder, int roundBorderColor, float roundBorderWidth) {
    115         this.hasRoundBorder = hasRoundBorder;
    116         this.roundBorderColor = roundBorderColor;
    117         this.roundBorderWidth = roundBorderWidth;
    118         initPaint();
    119         postInvalidate();
    120     }
    121 
    122 
    123     private int circleFillColor;
    124     private float circleRadius;
    125 
    126     private void initPaint() {
    127         paint_inner = new Paint();
    128         paint_inner.setColor(circleFillColor);
    129         paint_inner.setAntiAlias(true);//抗锯齿
    130         paint_inner.setStyle(Paint.Style.FILL);//FILL填充,stroke描边
    131 
    132         paint_border = new Paint();
    133         paint_border.setColor(roundBorderColor);
    134         paint_border.setAntiAlias(true);//抗锯齿
    135         paint_border.setStyle(Paint.Style.FILL);//FILL填充,stroke描边
    136     }
    137 
    138     //3个状态
    139     public static final int STATUS_NOT_CHECKED = 0x01;
    140     public static final int STATUS_CHECKED = 0x02;
    141     public static final int STATUS_CHECKED_ERR = 0x03;
    142 
    143     public void switchStatus(int status) {
    144         switch (status) {
    145             case STATUS_CHECKED:
    146                 circleFillColor = getResources().getColor(R.color.colorChecked);
    147                 roundBorderColor = getResources().getColor(R.color.colorRoundBorder);
    148                 break;
    149             case STATUS_CHECKED_ERR:
    150                 circleFillColor = getResources().getColor(R.color.colorCheckedErr);
    151                 roundBorderColor = getResources().getColor(R.color.colorRoundBorderErr);
    152                 break;
    153             case STATUS_NOT_CHECKED:// 普通状态
    154             default://以及缺省状态
    155                 //没有外框,内圈为灰色
    156                 circleFillColor = getResources().getColor(R.color.colorNotChecked);
    157                 roundBorderColor = getResources().getColor(R.color.transparent);
    158                 break;
    159         }
    160         initPaint();
    161         postInvalidate();
    162     }
    163 
    164 }

    然后是外层的布局:

      1 package com.example.gesture_password_study.gesture_pwd.custom;
      2 
      3 import android.content.Context;
      4 import android.content.res.TypedArray;
      5 import android.graphics.Canvas;
      6 import android.graphics.Paint;
      7 import android.graphics.Path;
      8 import android.graphics.Point;
      9 import android.graphics.Rect;
     10 import android.util.AttributeSet;
     11 import android.util.Log;
     12 import android.view.MotionEvent;
     13 import android.view.View;
     14 import android.widget.RelativeLayout;
     15 
     16 import com.example.gesture_password_study.R;
     17 
     18 import java.util.ArrayList;
     19 import java.util.List;
     20 
     21 /**
     22  * 手势密码绘制 控件;
     23  */
     24 public class EasyGestureLockLayout extends RelativeLayout {
     25 
     26     //全局变量统一管理
     27     private Context mContext;
     28     private boolean hasRoundBorder;//按键是否允许有圆环外圈
     29     private boolean ifAllowInteract;//是否允许有事件交互
     30     private Paint currentPaint;//当前使用的画笔
     31     private Paint paint_correct, paint_error;//画线用的两种颜色的画笔
     32     private GestureLockCircleView[] gestureCircleViewArr = null;//用数组来保存所有按键
     33     private int mCount = 4;// 方阵的行数(列数等同)
     34     private int mGesturePasswordViewWidth;//每一个按键的边长(因为宽高相同)
     35     private int mWidth, mHeight;//本layout的宽高
     36     private int childStartIndex, childEndIndex;//画轨迹线(密码轨迹)的时候,需要指定子的起始和结束 index
     37     private float marginRate = 0.2f;//缩小MotionEvent到达时的密码键选中的判定范围,这里的0.2的意思是,原本10*10的判定范围,现在,缩小到6*6,其他4,被两头平分
     38     private boolean ifAllowDrawLockPath = false;//因为有可能存在,down的时候没有点在任何一个键位的范围之内,所以必须用这个变量来控制是否进行绘制
     39     private int guideLineStartX, guideLineStartY, guideLineEndX, guideLineEndY;//引导线(正在画手势,但是尚未或者无法形成轨迹线的时候,会出现)的起始和终止坐标
     40     private int downX, downY;//MotionEvent的down事件坐标
     41     private int movedX, movedY;//MotionEvent的move事件坐标
     42     private Path lockPath = new Path();//密码的图形路径.用于绘制轨迹线
     43     private List<Integer> lockPathArr;//手势密码路径,用于输出到外界以及核对密码
     44     private int minLengthOfPwd = 4;//密码最少位数
     45 
     46     private int mModeStatus = -1;
     47     private List<Integer> checkPwd;//外界传入的需要核对的密码
     48     private int maxAttemptTimes = 5;//允许解锁的最大尝试次数,有必要的话,给他设置一个set方法,或者弄一个自定义属性
     49     private int currentAttemptTime = 1;// 当前尝试次数
     50 
     51     private int resetCurrentTime = 0;//当用户重新设置密码,这个值将会被重置
     52     private List<Integer> tempPwd;//用于重新设置密码
     53     private boolean ifCheckOnErr = false;//当前是否检测密码曾失败过
     54 
     55     //常量
     56     public static final int STATUS_RESET = 0x01;//本类状态:重新设置,此状态下会允许用户绘制两次手势,而且必须相同,绘制完成之后,返回密码值出去;
     57     // 如果第二次绘制和第一次绘制不同,则强制重新绘制
     58     public static final int STATUS_CHECK = 0x02;//本类状态:校验密码,此状态下,要求外界传入密码,然后给予用户若干尝试解锁的次数,
     59     // 如果规定次数之内,密码相同,则返回解锁成功;
     60     // 如果规定次数之内,都没有绘制出正确密码,则返回解锁失败;
     61 
     62     //************* 构造函数 *****************************
     63     public EasyGestureLockLayout(Context context) {
     64         this(context, null);
     65     }
     66 
     67     public EasyGestureLockLayout(Context context, AttributeSet attrs) {
     68         this(context, attrs, 0);
     69     }
     70 
     71     public EasyGestureLockLayout(Context context, AttributeSet attrs, int defStyleAttr) {
     72         super(context, attrs, defStyleAttr);
     73         dealAttr(context, attrs);
     74         init(context);
     75     }
     76 
     77     //************* 属性值获取 *****************************
     78     private void dealAttr(Context context, AttributeSet attrs) {
     79         TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.EasyGestureLockLayout);
     80 
     81         if (ta != null) {
     82             try {
     83                 hasRoundBorder = ta.getBoolean(R.styleable.EasyGestureLockLayout_ifChildHasBorder, false);
     84                 mCount = ta.getInteger(R.styleable.EasyGestureLockLayout_count, 3);
     85 
     86                 ifAllowInteract = ta.getBoolean(R.styleable.EasyGestureLockLayout_ifAllowInteract, false);
     87             } catch (Exception e) {
     88 
     89             } finally {
     90                 ta.recycle();
     91             }
     92         }
     93     }
     94 
     95     //************* 重写方法 *****************************
     96     @Override
     97     protected void onMeasure(int widthSpec, int heightSpec) {
     98         super.onMeasure(widthSpec, heightSpec);
     99 
    100         //取测量之后的宽和高
    101         mWidth = MeasureSpec.getSize(widthSpec);
    102         mHeight = MeasureSpec.getSize(heightSpec);
    103         //强行将绘图使用的宽高置为  测量宽高中的较小值, 因为绘图不能超出边界
    104         mHeight = mWidth = mWidth < mHeight ? mWidth : mHeight;
    105 
    106         // 初始化mGestureLockViews
    107         if (gestureCircleViewArr == null) {
    108             gestureCircleViewArr = new GestureLockCircleView[mCount * mCount];//用数组来保存 “按键”
    109             mGesturePasswordViewWidth = mWidth / mCount;//等分,不需要留间隙, 因为圆形控件会自己留空隙
    110 
    111             //利用相对布局的参数来放置子元素
    112             for (int i = 0; i < gestureCircleViewArr.length; i++) {
    113                 //初始化每个GestureLockView
    114                 gestureCircleViewArr[i] = getCircleView(mHeight);
    115                 gestureCircleViewArr[i].setId(i + 1);
    116                 LayoutParams lockerParams = new LayoutParams(
    117                         mGesturePasswordViewWidth, mGesturePasswordViewWidth);
    118 
    119                 // 不是每行的第一个,则设置位置为前一个的右边
    120                 if (i % mCount != 0) {
    121                     lockerParams.addRule(RelativeLayout.RIGHT_OF,
    122                             gestureCircleViewArr[i - 1].getId());
    123                 }
    124                 // 从第二行开始,设置为上一行同一位置View的下面
    125                 if (i > mCount - 1) {
    126                     lockerParams.addRule(RelativeLayout.BELOW,
    127                             gestureCircleViewArr[i - mCount].getId());
    128                 }
    129                 lockerParams.setMargins(0, 0, 0, 0);
    130                 addView(gestureCircleViewArr[i], lockerParams);
    131             }
    132         }
    133 
    134     }
    135 
    136     /**
    137      * 实验结果,在这里onDraw,绘制出来的线,总是会被子元素覆盖,
    138      *
    139      * @param canvas
    140      */
    141     @Override
    142     protected void onDraw(Canvas canvas) { //闹半天,这个onDraw没有执行
    143         super.onDraw(canvas);
    144         //奇怪,为何不执行onDraw
    145         // 一般情况下,viewGroup都不会执行onDraw,因为它本身是一个容器,容器不具有自我绘制功能;
    146         //图像的表现,和绘制的顺序有关系;
    147         Log.d("onDrawTag", "onDraw");
    148     }
    149 
    150     /**
    151      * 然而,由这个方法进行绘制,线,则会覆盖"子";
    152      *
    153      * @param canvas
    154      */
    155     @Override
    156     public void dispatchDraw(Canvas canvas) {
    157         super.dispatchDraw(canvas);//这一步居然就是绘制 “子”, 具体看View.java 的 19195行
    158         Log.d("onDrawTag", "dispatchDraw");//那么, 等children画完了之后,再画线,就名正言顺了。⊙︿⊙ 一头包。明白了
    159         if (gestureCircleViewArr != null && ifAllowInteract) {
    160             drawLockPath(canvas);
    161             drawMovingPath(canvas);
    162         }
    163     }
    164 
    165     //************* 模式设置 *****************************
    166 
    167     public int getCurrentMode() {
    168         return mModeStatus;
    169     }
    170 
    171     /**
    172      * 切换到Reset模式,重新设置手势密码;
    173      * 此模式下,不需要入参。设置完成之后,会执行回调GestureEventCallback.onResetFinish(pwd);
    174      */
    175     public void switchToResetMode() {
    176         mModeStatus = STATUS_RESET;
    177     }
    178 
    179     /**
    180      * 切换到 校验模式;
    181      * 这个模式需要传入原始密码,以及最大尝试的次数;
    182      * <p>
    183      * 尝试解锁成功,或者超过了最大尝试次数都没有成功,就会执行回调GestureEventCallback.onCheckFinish(boolean succeedOrFailed);
    184      *
    185      * @param pwd
    186      * @param maxAttemptTimes
    187      */
    188     public void switchToCheckMode(List<Integer> pwd, int maxAttemptTimes) {
    189         if (pwd == null || maxAttemptTimes <= 0) {
    190             Log.e("switchToCheckMode", "参数错误,pwd不能为空,而且 maxAttemptTimes必须大于0");
    191             return;
    192         }
    193         this.currentAttemptTime = 1;
    194         this.mModeStatus = STATUS_CHECK;
    195         this.maxAttemptTimes = maxAttemptTimes;
    196         this.checkPwd = copyPwd(pwd);
    197     }
    198 
    199     //****************************以下全是业务代码**************************
    200     private int background_color = 0xff4790FF;
    201     private int background_color_transparent = 0x00000000;
    202 
    203     /**
    204      * 初始化画笔,
    205      *
    206      * @param context
    207      */
    208     private void init(Context context) {
    209         mContext = context;
    210         setClickable(true);//为了顺利接收事件,需要开启click;因为你如果不设置,,就只能收到down,其他的一概收不到
    211         setBackgroundColor(background_color_transparent);//设置透明色;这里如果不设置,onDraw将不会执行;原因:这是一个ViewGroup,本身是容器,不具备自我绘制功能,但是这里设置了背景色,就说明有东西需要绘制,onDraw就会执行;
    212 
    213         paint_correct = new Paint();
    214         paint_correct.setStyle(Paint.Style.STROKE);
    215         paint_correct.setAntiAlias(true);
    216         paint_correct.setColor(getResources().getColor(R.color.colorChecked));
    217 
    218         paint_error = new Paint();
    219         paint_error.setStyle(Paint.Style.STROKE);
    220         paint_error.setAntiAlias(true);
    221         paint_error.setColor(getResources().getColor(R.color.colorCheckedErr));
    222 
    223         initLockPathArr();
    224         currentPaint = paint_correct;// 默认使用的画笔
    225     }
    226 
    227     /**
    228      * 构建单个圆
    229      *
    230      * @param wh 边长
    231      * @return
    232      */
    233     private GestureLockCircleView getCircleView(int wh) {
    234         GestureLockCircleView gestureCircleView = new GestureLockCircleView(mContext);
    235 
    236         double s = Math.pow(mCount, 3) + 0.5f;//除法系数,用于计算内圆的半径; 行数的3次方,并且转为浮点型
    237         gestureCircleView.setInnerCircle(getResources().getColor(R.color.colorChecked), (float) (wh / s));
    238 
    239         paint_correct.setStrokeWidth((float) (wh / s) * 0.2f);
    240         paint_error.setStrokeWidth((float) (wh / s) * 0.2f);
    241 
    242         //内圆颜色,内圆半径
    243         s = Math.pow(mCount, 2) + 0.5f;//除法系数,用于计算外圆的半径;行数的2次方,并且转为浮点型
    244         gestureCircleView.setBorderRound(hasRoundBorder, getResources().getColor(R.color.colorChecked), (float) (wh / s));//是否有边框,外圆颜色,外圆半径
    245         gestureCircleView.switchStatus(GestureLockCircleView.STATUS_NOT_CHECKED);
    246         return gestureCircleView;
    247     }
    248 
    249     /**
    250      * 重置所有按键为 notChecked 状态
    251      */
    252     private void resetAllCircleBtn() {
    253         if (gestureCircleViewArr == null) return;
    254         for (int i = 0; i < gestureCircleViewArr.length; i++) {
    255             gestureCircleViewArr[i].switchStatus(GestureLockCircleView.STATUS_NOT_CHECKED);
    256         }
    257     }
    258 
    259     //*************************手势密码路径的管理***********************************************
    260     private void initLockPathArr() {
    261         lockPathArr = new ArrayList<>();
    262     }
    263 
    264     /**
    265      * 增加一个密码数字
    266      *
    267      * @param p
    268      */
    269     private void addPwd(int p) {
    270         if (!checkRepetition(p)) {
    271             lockPathArr.add(p);
    272         }
    273     }
    274 
    275     private void resetPwd() {
    276         if (lockPathArr == null)
    277             lockPathArr = new ArrayList<>();
    278         else
    279             lockPathArr.clear();
    280     }
    281 
    282     /**
    283      * 绘制密码“轨迹线”
    284      *
    285      * @param canvas
    286      */
    287     private void drawLockPath(Canvas canvas) {
    288         canvas.drawPath(lockPath, currentPaint);
    289     }
    290 
    291     /**
    292      * 重置引导线的起/终 坐标值
    293      */
    294     private void resetMovingPathCoordinate() {
    295         guideLineStartX = 0;
    296         guideLineStartY = 0;
    297         guideLineEndX = 0;
    298         guideLineEndY = 0;
    299     }
    300 
    301     /**
    302      * 绘制引导线
    303      */
    304     private void drawMovingPath(Canvas canvas) {
    305         if (guideLineStartX != 0 && guideLineStartY != 0)//只有当起始位置不是0的时候,才进行绘制
    306             canvas.drawLine(guideLineStartX, guideLineStartY, guideLineEndX, guideLineEndY, currentPaint);
    307     }
    308 
    309     /**
    310      * 辅助方法,获得一个View的中心位置
    311      *
    312      * @param v
    313      * @return
    314      */
    315     private Point getCenterPoint(View v) {
    316         Rect rect = new Rect();
    317         v.getHitRect(rect);
    318         int x = rect.left + v.getWidth() / 2;
    319         int y = rect.top + v.getHeight() / 2;
    320         return new Point(x, y);
    321     }
    322 
    323     /**
    324      * 判断当前点击的点位置是不是在子元素范围之内
    325      *
    326      * @param x
    327      * @param y
    328      * @param v
    329      * @return
    330      */
    331     private boolean ifClickOnView(int x, int y, View v) {
    332         Rect r = new Rect();
    333         v.getHitRect(r);
    334 
    335         //判定点是不是在view范围内,根据业务需求,要给view一个判定的间隙,比如 5*5的View,判定范围只能是3*3
    336         //以原来的矩阵为基础,重新定一个判定范围,范围暂时定位原来的80%
    337         //真正的判定区域的矩阵范围
    338 
    339         int w = v.getWidth();
    340         int h = v.getHeight();
    341 
    342         int realLeft = (int) (r.left + marginRate * w);
    343         int realTop = (int) (r.top + marginRate * h);
    344         int realRight = (int) (r.right - marginRate * w);
    345         int realBottom = (int) (r.bottom - marginRate * h);
    346 
    347         Rect rect1 = new Rect(realLeft, realTop, realRight, realBottom);
    348 
    349         if (rect1.contains(x, y)) {
    350             return true;
    351         }
    352         return false;
    353     }
    354 
    355     /**
    356      * 根据点坐标,返回当前点在哪个密码键的范围内,直接返回View对象
    357      *
    358      * @param x
    359      * @param y
    360      * @return
    361      */
    362     private GestureLockCircleView getClickedChild(int x, int y) {
    363         for (GestureLockCircleView v : gestureCircleViewArr) {
    364             if (ifClickOnView(x, y, v)) {//
    365                 return v;
    366             }
    367         }
    368         return null;
    369     }
    370 
    371     /**
    372      * 根据点坐标,返回当前点在哪个密码键的范围内,直接返回View对象的id
    373      *
    374      * @param x
    375      * @param y
    376      * @return
    377      */
    378     private int getClickedChildIndex(int x, int y) {
    379         for (int i = 0; i < gestureCircleViewArr.length; i++) {
    380             View v = gestureCircleViewArr[i];
    381             if (ifClickOnView(x, y, v)) {//
    382                 return i;
    383             }
    384         }
    385         return -1;
    386     }
    387 
    388     /**
    389      * 检查密码值是否重复
    390      *
    391      * @return
    392      */
    393     private boolean checkRepetition(int pwd) {
    394         return lockPathArr.contains(pwd);
    395     }
    396 
    397     /**
    398      * 手势绘制
    399      *
    400      * @param event
    401      * @return
    402      */
    403     @Override
    404     public boolean onTouchEvent(MotionEvent event) {
    405         if (ifAllowInteract)//只有设置了允许事件交互,才往下执行
    406             switch (event.getAction()) {
    407                 case MotionEvent.ACTION_DOWN:
    408                     onToast("", ColorHolder.COLOR_GRAY);
    409                     downX = (int) event.getX();
    410                     downY = (int) event.getY();
    411                     ifAllowDrawLockPath = false;
    412                     GestureLockCircleView current = getClickedChild(downX, downY);
    413                     if (current != null) {//如果当前按下的点,没有在任何一个按键范围之内
    414                         ifAllowDrawLockPath = true;
    415 
    416                         if (ifCheckOnErr)
    417                             current.switchStatus(GestureLockCircleView.STATUS_CHECKED_ERR);
    418                         else
    419                             current.switchStatus(GestureLockCircleView.STATUS_CHECKED);//down的时候,将当前这个按键设置为checked
    420 
    421                         childStartIndex = getClickedChildIndex(downX, downY);
    422                         //记录手势密码
    423                         lockPath.reset();
    424                         resetPwd();
    425                         addPwd(childStartIndex);
    426                         //path处理
    427                         Point startP = getCenterPoint(gestureCircleViewArr[childStartIndex]);
    428                         if (startP != null) {//因为如果
    429                             lockPath.moveTo(startP.x, startP.y);
    430                             //引导线的起始坐标
    431                             guideLineStartX = startP.x;
    432                             guideLineStartY = startP.y;
    433                         } else {
    434                             Log.d("tagpx", "1");
    435                         }
    436                     } else {
    437                         //如果第一次点下去,就是在 键位的空隙里面。那么,就不用绘制了
    438                         Log.d("tagpx", "2");
    439                     }
    440 
    441                     break;
    442                 case MotionEvent.ACTION_MOVE:
    443                     if (ifAllowDrawLockPath) {
    444                         movedX = (int) event.getX();
    445                         movedY = (int) event.getY();
    446                         childEndIndex = getClickedChildIndex(movedX, movedY);
    447 
    448                         //-1表示没有找到对应的区域
    449                         boolean flag1 = childStartIndex != -1 && childEndIndex != -1;//没有获取到正确的对应区域
    450                         boolean flag2 = childStartIndex != childEndIndex;//在同一个区域内不需要画线
    451                         boolean flag3 = checkRepetition(childEndIndex);//不允许密码值重复,这里要检查当前这个区域是不是已经在lockPathArr里面
    452 
    453                         if (flag1 && flag2 && !flag3) {//如果起点终点都在区域之内,那么就直接绘制“轨迹线”
    454                             Point endP = getCenterPoint(gestureCircleViewArr[childEndIndex]);
    455                             GestureLockCircleView cur = getClickedChild(movedX, movedY);
    456                             if (ifCheckOnErr)
    457                                 cur.switchStatus(GestureLockCircleView.STATUS_CHECKED_ERR);
    458                             else
    459                                 cur.switchStatus(GestureLockCircleView.STATUS_CHECKED);
    460 
    461                             addPwd(childEndIndex);
    462                             lockPath.lineTo(endP.x, endP.y);
    463 
    464                             guideLineStartX = endP.x;
    465                             guideLineStartY = endP.y;
    466                         }
    467                         guideLineEndX = movedX;
    468                         guideLineEndY = movedY;
    469                         postInvalidate();//刷新视图
    470                     }
    471                     break;
    472                 case MotionEvent.ACTION_UP:
    473                 case MotionEvent.ACTION_CANCEL:
    474                     if (ifAllowDrawLockPath) {
    475                         resetMovingPathCoordinate(); // up的时候,要清除引导线
    476                         lockPath.reset(); //同时要清除轨迹线
    477                         postInvalidate();//刷新本layout
    478                         resetAllCircleBtn();//up的时候,把所有按键全部设置为notChecked,
    479                         onSwipeFinish();
    480                         if (lockPathArr.size() >= minLengthOfPwd) {
    481                             if (mModeStatus == STATUS_RESET) {//如果处于reset模式下,执行rest的回调
    482                                 onReset();
    483                             } else if (mModeStatus == STATUS_CHECK) {//检查模式下,执行onCheck
    484                                 onCheck();
    485                             } else {
    486                                 throw new RuntimeException("异常模式,请正确调用switchToCheckMode/switchToResetMode!");
    487                             }
    488                         } else {
    489                             onToast(String.format(ToastStrHolder.swipeTooLittlePointStr, minLengthOfPwd), ColorHolder.COLOR_RED);
    490                         }
    491                     }
    492                     break;
    493                 default:
    494                     break;
    495             }
    496         return super.onTouchEvent(event);
    497     }
    498 
    499     private void onSwipeFinish() {
    500         if (mGestureEventCallback == null) return;
    501         mGestureEventCallback.onSwipeFinish(copyPwd(lockPathArr));
    502     }
    503 
    504     private void onReset() {
    505         if (mGestureEventCallback == null) return;
    506         if (resetCurrentTime == 0) {//第一次绘制,赋值给tempPwd
    507             tempPwd = copyPwd(lockPathArr);
    508             resetCurrentTime++;
    509             onToast(ToastStrHolder.tryAgainStr, ColorHolder.COLOR_GRAY);
    510         } else {
    511             try {
    512                 boolean s = compare(tempPwd, lockPathArr);
    513                 if (s) {
    514                     onToast(ToastStrHolder.successStr, ColorHolder.COLOR_GRAY);
    515                     mGestureEventCallback.onResetFinish(copyPwd(lockPathArr));//执行回调
    516                 } else {
    517                     onToast(ToastStrHolder.notSameStr, ColorHolder.COLOR_RED);
    518                 }
    519             } catch (RuntimeException e) {
    520                 e.printStackTrace();
    521             }
    522         }
    523     }
    524 
    525     /**
    526      * 初始化当前的绘制次数
    527      */
    528     public void initCurrentTimes() {
    529         resetCurrentTime = 0;
    530     }
    531 
    532     private void onCheck() {
    533         if (mGestureEventCallback == null) return;
    534         boolean compareRes = compare(checkPwd, lockPathArr); //对比当前密码和外界传入的密码
    535         if (currentAttemptTime <= maxAttemptTimes) {//如果还能继续尝试解锁,那么
    536             if (compareRes) {//如果成功
    537                 mGestureEventCallback.onCheckFinish(compareRes);//直接返回结果
    538 
    539                 currentAttemptTime = 1;
    540                 currentPaint = paint_correct;
    541                 ifCheckOnErr = false;
    542             } else {//否则,提示
    543                 int remindTime = maxAttemptTimes - currentAttemptTime;
    544                 if (remindTime > 0) {
    545                     onToast(String.format(ToastStrHolder.wrongPwdInputStr, remindTime), ColorHolder.COLOR_RED);
    546 
    547                     currentPaint = paint_error;
    548                     ifCheckOnErr = true;
    549                 } else {
    550                     mGestureEventCallback.onCheckFinish(compareRes);//直接返回结果
    551                 }
    552                 currentAttemptTime++;
    553             }
    554         } else {//如果已经不能尝试, 无论是否成功,都要返回结果
    555             mGestureEventCallback.onCheckFinish(compareRes);
    556             currentAttemptTime = 1;
    557         }
    558     }
    559 
    560     private void onSwipeMore() {
    561         if (mGestureEventCallback == null) return;
    562         mGestureEventCallback.onSwipeMore();
    563     }
    564 
    565     private void onToast(String s, int color) {
    566         if (mGestureEventCallback == null) return;
    567         mGestureEventCallback.onToast(s, color);
    568     }
    569 
    570     /**
    571      * 提供一个方法,绘制密码点,但是只绘制 圆圈,不绘制引导线和轨迹线
    572      */
    573     public void refreshPwdKeyboard(List<Integer> pwd) {
    574         try {
    575             for (int i = 0; i < mCount * mCount; i++) {//先把所有的点都设置为notChecked
    576                 gestureCircleViewArr[i].switchStatus(GestureLockCircleView.STATUS_NOT_CHECKED);
    577             }
    578 
    579             if (null != pwd)
    580                 for (int i = 0; i < pwd.size(); i++) {//再把密码中的点,设置为checked
    581                     gestureCircleViewArr[pwd.get(i)].switchStatus(GestureLockCircleView.STATUS_CHECKED);
    582                 }
    583         } catch (IndexOutOfBoundsException e) {
    584             //这里有可能发生数组越界,因为 本类的各个对象时相互独立的,方阵行数可能不同
    585             e.printStackTrace();
    586         }
    587     }
    588 
    589     //*************************下面业务对接***********************************************
    590     public interface GestureEventCallback {
    591         /**
    592          * 当滑动结束,无论模式,只要滑动之后发现upEvent就执行
    593          */
    594         void onSwipeFinish(List<Integer> pwd);
    595 
    596         /**
    597          * 当重新设置密码成功的时候,将密码返回出去
    598          *
    599          * @param pwd 设置的密码
    600          */
    601         void onResetFinish(List<Integer> pwd);
    602 
    603         /**
    604          * 如果当前模式是 check模式,则用这个方法来返回check的结果
    605          *
    606          * @param succeedOrFailed 校验是否成功
    607          */
    608         void onCheckFinish(boolean succeedOrFailed);
    609 
    610         /**
    611          * 如果当前滑动的密码格子数太少(比如设置了至少滑动4格,却只滑了2格)
    612          */
    613         void onSwipeMore();
    614 
    615         /**
    616          * 当需要给外界反馈信息的时候
    617          *
    618          * @param s     信息内容
    619          * @param color 有必要的话,传字体颜色给外界
    620          */
    621         void onToast(String s, int color);
    622     }
    623 
    624     /**
    625      * 反馈给外界的回调
    626      */
    627     private GestureEventCallback mGestureEventCallback;
    628 
    629     public void setGestureFinishedCallback(GestureEventCallback gestureFinishedCallback) {
    630         this.mGestureEventCallback = gestureFinishedCallback;
    631     }
    632 
    633     public static class GestureEventCallbackAdapter implements GestureEventCallback {
    634 
    635         @Override
    636         public void onSwipeFinish(List<Integer> pwd) {
    637 
    638         }
    639 
    640         @Override
    641         public void onResetFinish(List<Integer> pwd) {
    642 
    643         }
    644 
    645         @Override
    646         public void onCheckFinish(boolean succeedOrFailed) {
    647 
    648         }
    649 
    650         @Override
    651         public void onSwipeMore() {
    652 
    653         }
    654 
    655         @Override
    656         public void onToast(String s, int color) {
    657 
    658         }
    659     }
    660 
    661     //*************************下面是辅助方法以及辅助内部类***********************************************
    662 
    663     /**
    664      * 辅助方法,复制一份密码对象,因为如果直接把当前对象的密码返回出去,则外界使用的全部都是同一个对象,这个对象可能随时变化,外层逻辑无法对比密码值
    665      */
    666     private List<Integer> copyPwd(List<Integer> pwd) {
    667         List<Integer> copyOne = new ArrayList<>();
    668         for (int i = 0; i < pwd.size(); i++) {
    669             copyOne.add(pwd.get(i));
    670         }
    671         return copyOne;
    672     }
    673 
    674     /**
    675      * 对比两个list是否内容完全相同
    676      */
    677     private boolean compare(List<Integer> list1, List<Integer> list2) throws RuntimeException {
    678 
    679         if (list1 == null || list2 == null) {
    680             throw new RuntimeException("存在list为空,不执行对比");
    681         }
    682 
    683         if (list1.size() != list2.size())//size长度都不同,就不用比了
    684             return false;
    685 
    686         for (int i = 0; i < list1.size(); i++) {
    687             if (list1.get(i) != list2.get(i)) {
    688                 return false;
    689             }
    690         }
    691         return true;
    692     }
    693 
    694 
    695     public class ColorHolder {
    696         public static final int COLOR_RED = 0xffFF3232;
    697         public static final int COLOR_GRAY = 0xff999999;
    698         public static final int COLOR_YELLOW = 0xffF8A916;
    699     }
    700 
    701     public class ToastStrHolder {
    702         public static final String successStr = "绘制成功";
    703         public static final String tryAgainStr = "请再次绘制手势密码";
    704         public static final String notSameStr = "与首次绘制不一致,请再次绘制";
    705         public static final String forYourSafetyStr = "为了您的账户安全,请设置手势密码";
    706         public static final String swipeTooLittlePointStr = "请最少连接%s个点";
    707         public static final String wrongPwdInputStr = "输入错误,您还可以输入%s次";
    708     }
    709 }

     具体使用方法:


    只展示一个例子,这是设置手势密码的界面,红色的代码就是你需要自己编写的;

    package com.example.gesture_password_study.gesture_pwd;
    
    
    import android.os.Bundle;
    import android.support.annotation.Nullable;
    import android.view.View;
    import android.widget.TextView;
    import android.widget.Toast;
    
    import com.example.gesture_password_study.R;
    import com.example.gesture_password_study.gesture_pwd.base.GestureBaseActivity;
    import com.example.gesture_password_study.gesture_pwd.custom.EasyGestureLockLayout;
    
    import java.util.List;
    
    /**
     * 手势密码 设置界面
     */
    public class GesturePwdSettingActivity extends GestureBaseActivity {
    
        EasyGestureLockLayout layout_small;
        TextView tv_go;
        TextView tv_redraw;
        EasyGestureLockLayout layout_parent;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.layout_gesture_pwd_setting);
            initView();
            initLayoutView();
        }
    
        private void initView() {
            tv_go = findViewById(R.id.tv_go);
            layout_parent = findViewById(R.id.layout_parent);
            layout_small = findViewById(R.id.layout_small);
            tv_redraw = findViewById(R.id.tv_redraw);
        }
    
    
        protected void initLayoutView() {
            
            //写个适配器
            EasyGestureLockLayout.GestureEventCallbackAdapter adapter = new EasyGestureLockLayout.GestureEventCallbackAdapter() {
                @Override
                public void onSwipeFinish(List<Integer> pwd) {
                    layout_small.refreshPwdKeyboard(pwd);//通知另一个小密码盘,将密码点展示出来,但是不展示轨迹线
                    tv_redraw.setVisibility(View.VISIBLE);
                }
    
                @Override
                public void onResetFinish(List<Integer> pwd) {// 当密码设置完成
                    savePwd(showPwd("showGesturePwdInt", pwd));//保存密码到本地
                    Toast.makeText(GesturePwdSettingActivity.this, "密码已保存", Toast.LENGTH_SHORT).show();
                }
    
                @Override
                public void onCheckFinish(boolean succeedOrFailed) {
                    String str = succeedOrFailed ? "解锁成功" : "解锁失败";
                    Toast.makeText(GesturePwdSettingActivity.this, str, Toast.LENGTH_SHORT).show();
                    if (succeedOrFailed) {//如果解锁成功,则切换到set模式
                        layout_parent.switchToResetMode();
                    } else {
                        onCheckFailed();
                    }
                }
    
                @Override
                public void onSwipeMore() {
                    //执行动画
                    animate(tv_go);
                }
    
                @Override
                public void onToast(String s, int textColor) {
                    tv_go.setText(s);
                    if (textColor != 0)
                        tv_go.setTextColor(textColor);
    
                    if (textColor == 0xffFF3232) {
                        animate(tv_go);
                    }
                }
            };
    
            layout_parent.setGestureFinishedCallback(adapter);
    
            //使用rest模式
            layout_parent.switchToResetMode();
    
            tv_redraw.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    layout_parent.initCurrentTimes();
                    tv_redraw.setVisibility(View.INVISIBLE);
                    layout_small.refreshPwdKeyboard(null);
                    tv_go.setText("请重新绘制");
                }
            });
        }
    
    
    
    }

    它的布局xml:

    layout_gesture_pwd_setting.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        android:gravity="center_horizontal"
        android:orientation="vertical">
    
        <TextView
            android:id="@+id/tv_skip"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right"
            android:layout_marginBottom="16dp"
            android:layout_marginRight="20dp"
            android:layout_marginTop="40dp"
            android:text="--"
            android:textColor="@color/color_v"
            android:textSize="15sp" />
    
    
        <com.example.gesture_password_study.gesture_pwd.custom.EasyGestureLockLayout
            android:id="@+id/layout_small"
            android:layout_width="@dimen/small_grid_width"
            android:layout_height="@dimen/small_grid_width"
            app:count="3"
            app:ifAllowInteract="false"
            app:ifChildHasBorder="false" />
    
        <TextView
            android:id="@+id/tv_go"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="为了您的账户安全,请设置手势密码"
            android:textColor="#F8A916"
            android:textSize="13sp" />
    
        <com.example.gesture_password_study.gesture_pwd.custom.EasyGestureLockLayout
            android:id="@+id/layout_parent"
            android:layout_width="@dimen/big_grid_width"
            android:layout_height="@dimen/big_grid_width"
            android:layout_marginTop="64dp"
            app:count="3"
            app:ifAllowInteract="true"
            app:ifChildHasBorder="true" />
    
        <TextView
            android:id="@+id/tv_redraw"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="重新绘制"
            android:textColor="@color/color_v"
            android:textSize="15sp"
            android:visibility="invisible"/>
    
    
    </LinearLayout>

    count属性,是控制 密码盘的 方阵宽度,目前是3,所以呈现出来就是3*3;

    你可以换成4,5,6···随意,只要你没有密集恐惧症.```````````

     ===========================================

     欧拉,源码解读就到这里,也没什么复杂的东西。

    想起之前面试的时候有一个大佬问我的问题, 自定义ViewGroup能不能在里面同时放置子View并且还能对自身进行绘制。

    当时一脸懵逼,不知道什么意思,···· 现在知道了。

    自定义ViewGroup,然后addView。。。然后还 onDraw··自己。

     

    喜欢的大佬可以下载源码,欢迎留言讨论···

  • 相关阅读:
    JavaScript打造很酷的图片放大效果实例代码
    【荐】CSS实现的鼠标点击小图无刷新放大图片代码
    JavaScript+CSS实现的文字幻灯切换代码
    【荐】很棒的图片友情链接带控制按钮的横向滚动代码
    jquery制作一个漂亮带渐隐效果的跑动区域
    JS打造的一款响应鼠标变化很炫的图片特效代码
    JS+CSS控制鼠标移上图片滑出文字提示代码
    Jquery+CSS实现的大气漂亮图片切换效果代码
    【荐】JS+CSS防FLASH效果带倒影的图片切换效果代码
    JavaScript限制对图片右键代码
  • 原文地址:https://www.cnblogs.com/hankzhouAndroid/p/9590583.html
Copyright © 2020-2023  润新知