自定義手勢密碼控件

說明

手勢密碼這個有2種方法實現(xiàn),一個是直接繼承view然后在里面畫出來,并做事件處理。另一種就是用viewgroup來包了。奈何本人沒上過數(shù)學,所以我采取后者。

先看效果圖

錯誤
成功

分析實現(xiàn):
上面的指示部分后面在說,先看主體部分

  • 里面的每一個格子是一個單獨的自定義view,隨便取個名字叫做lockView吧。
    這個lockview有四種狀態(tài):

    • 普通NORMAL
    • 按下DOWN
    • 錯誤ERROR
    • 成功SUCCESS

    在繪制的時候根據(jù)不同狀態(tài)繪制不同效果。

  • 外面則是一個viewgroup 取名叫GestureLockView,包裹著9個lockview.亂摸事件處理都在這個viewgroup中。

    大概就是這樣。下面是具體代碼實現(xiàn).注釋相當詳細。

/**
每一個小格子view
*/
public class LockView extends View {
  //初始狀態(tài)
  public static final int NORMAL = 914;
  //鼠標按下
  public static final int DOWN = 669;
  //密碼錯誤
  public static final int ERROR = 873;
  //密碼成功
  public static final int SUCCESS = 440;

  private static final String TAG = LockView.class.getSimpleName();

  private
  @Status
  int status = NORMAL;

  private Paint mPaint;


  public LockView(Context context) {
      super(context);
      init();
  }

  public LockView(Context context, AttributeSet attrs) {
      super(context, attrs);

      init();
  }

  private void init() {
      mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mPaint.setStrokeWidth(2);
  }
 
  /** 
  不想用枚舉,這玩意好
  */
  @IntDef({NORMAL, DOWN, ERROR,SUCCESS})
  @Retention(RetentionPolicy.SOURCE)
  public @interface Status {
  }

  @Override
  protected void onDraw(Canvas canvas) {
      super.onDraw(canvas);

      if (status == ERROR) {
          //密碼錯誤時,狀態(tài)顏色為紅色
          mPaint.setColor(Color.RED);
      }else if (status == DOWN){
        //正在繪制中,狀態(tài)顏色為綠色
          mPaint.setColor(Color.GREEN);
      }else if (status == SUCCESS){
        //成功
          mPaint.setColor(Color.parseColor("#0094ff"));
      } else {
          mPaint.setColor(Color.WHITE);
      }
      mPaint.setStyle(Paint.Style.STROKE);
      int center = getMeasuredHeight() / 2;
      int radius = getWidth()/2;
      //外圓
      canvas.drawCircle(center, center, radius, mPaint);

      if (status == ERROR || status == SUCCESS) {
          //2條弧
          if (status == ERROR){

              mPaint.setColor(Color.RED);
          }else{
              mPaint.setColor(Color.parseColor("#0094ff"));
          }
          int r = radius / 2;
          RectF rectF = new RectF(center - r, center - r, center + r, center + r);
          canvas.drawArc(rectF, 70, 160, false, mPaint);
          canvas.drawArc(rectF, -100, 140, false, mPaint);
          //實心圓
          mPaint.setStyle(Paint.Style.FILL);
          canvas.drawCircle(center, center, radius / 8, mPaint);
      }

  }

  /** 設置當前view狀態(tài) */
  public void setStatus(@Status int status) {
      this.status = status;
      invalidate();
  }
}

下面是ViewGroup

/** 手勢密碼容器 */
public class GestureLockView extends ViewGroup {

   private static final String TAG = GestureLockView.class.getSimpleName();
   private int colNum = 3;
   /**
    * 設置的9宮格密碼。當前view的id
    */
   private final ArrayList<Integer> passwd = new ArrayList<>();
   /**
    * 當前選擇的密碼。當前view的id
    */
   private final ArrayList<Integer> choose = new ArrayList<>();
   /**
    * 設置的密碼長度
    */
   private int passwdLen;

   /**
    * 以2點為key,它們之間的中間點為value.用于記錄是否需要自動連接中間點
    */
   private ArrayMap<String, Integer> betweenMap;

   private LockListener mListener;

   public GestureLockView(Context context, AttributeSet attrs) {
       super(context, attrs);
       betweenMap = new ArrayMap<>();
       //映射2點與中間點
       betweenMap.put("1,3", 2);
       betweenMap.put("1,7", 4);
       betweenMap.put("1,9", 5);
       betweenMap.put("2,8", 5);
       betweenMap.put("3,1", 2);
       betweenMap.put("3,7", 5);
       betweenMap.put("3,9", 6);
       betweenMap.put("4,6", 5);
       betweenMap.put("7,1", 4);
       betweenMap.put("7,3", 5);
       betweenMap.put("7,9", 8);
       betweenMap.put("8,2", 5);
       betweenMap.put("9,1", 5);
       betweenMap.put("9,3", 6);
       betweenMap.put("9,7", 8);


   }

   /** 根據(jù)子view來測量 */
   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {


       final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
       final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
       final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
       final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

       int resultWidth = 0;
       int resultHeight = 0;

       //添加9格view
       int count = 9;

       //默認margin
       int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getContext().getResources().getDisplayMetrics());
       
       //默認每個小格子寬度
       int itemWidth = 200;

       if (widthMode == MeasureSpec.EXACTLY) {
           itemWidth = widthSize / colNum - margin;
       }


       //在添加子view前先清空
       removeAllViews();

       for (int i = 0; i < count; i++) {
           int row = i / colNum; //當前view所在行
           int col = i % colNum; //當前view所在列

           //創(chuàng)建每個圖案
           LockView lockView = new LockView(getContext());
           MarginLayoutParams lp = new MarginLayoutParams(itemWidth, itemWidth);
           lp.leftMargin = margin;
           lp.topMargin = margin;
           //添加一個標記
           lockView.setId(i + 1);

           addView(lockView, lp);

           measureChild(lockView, widthMeasureSpec, heightMeasureSpec);

           if (widthMeasureSpec != MeasureSpec.EXACTLY) {

               //寬度取第一行。每個view寬度相+margin
               if (row == 0) {
                   resultWidth += lockView.getMeasuredWidth() + margin; //左margin
               }
           }
           if (heightMeasureSpec != MeasureSpec.EXACTLY) {
               //高度取第一列的view高+margin
               if (col == 0) {
                   resultHeight += lockView.getMeasuredHeight() + margin; //margin
               }
           }

       }

       resultWidth = resultWidth == 0 ? widthSize : resultWidth;
       resultHeight = resultHeight == 0 ? heightSize : resultHeight;

       setMeasuredDimension(resultWidth, resultHeight);

   }

 /** 按9宮格擺放 */
   @Override
   protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

       final int childCount = getChildCount();

       for (int i = 0; i < childCount; i++) {
           int col = i % colNum;
           int row = i / colNum;

           final View view = getChildAt(i);

           final int childWidth = view.getMeasuredWidth();
           final int childHeight = view.getMeasuredHeight();
           MarginLayoutParams mp = (MarginLayoutParams) view.getLayoutParams();

           int l, t, r, b;
           /*
            計算位置,套一下數(shù)值擺擺就知道是什么意思了
            */
           l = col * childWidth + mp.leftMargin * (col + 1);
           t = row * childHeight + mp.topMargin * (row + 1);
           r = childWidth + l;
           b = childHeight + t;

           view.layout(l, t, r, b);

       }

   }


   /**
    * 繪制路徑用的
    */
   private Path linePath = new Path();
   /**
    * 當前開始的view
    */
   private LockView currentStartView;

   @Override
   public boolean onTouchEvent(MotionEvent event) {
       int x = (int) event.getX();
       int y = (int) event.getY();
       int count = getChildCount();
       switch (event.getAction()) {
           case MotionEvent.ACTION_DOWN:
               //每次按下時都表示從新開始,清除所有view的狀態(tài)
               for (int i = 0; i < count; i++) {
                   ((LockView) getChildAt(i)).setStatus(LockView.NORMAL);
               }
               choose.clear();
               linePath.reset();
               //根據(jù)按下的xy,獲取對應位置的view,有可能按的不在veiw的位置上,所以可能會為null
               currentStartView = getChildByPos(x, y);
               break;
           case MotionEvent.ACTION_MOVE:
               //手指開始亂摸,設置路徑線顏色為紅色
               linePaint.setColor(Color.RED);

               final LockView currentPosView = getChildByPos(x, y);

               if (currentPosView != null) {
                   //有可能在按下的時候不在正確的view區(qū)域,所以會為null
                   if (currentStartView == null) {
                       currentStartView = currentPosView;
                   }
                   //2個view之間是否有需要連接的中間view
                   final int pos = getBetween(currentStartView, currentPosView);


                   if (pos != -1) {
                       //有需要進行連接的中間節(jié)點
                       LockView betweenView = (LockView) getChildAt(pos - 1);
                       //設置其選擇狀態(tài)
                       betweenView.setStatus(LockView.DOWN);
                       if (!choose.contains(betweenView.getId())) {
                           //添加到集合
                           choose.add(betweenView.getId());
                       }
                   }


                   int size = choose.size();
                   final int childViewId = currentPosView.getId();

                   //不保存連續(xù)相同的密碼
                   if ((0 == size) ||
                           (choose.get(size - 1) != childViewId)) {
                       choose.add(childViewId);

                       currentPosView.setStatus(LockView.DOWN);
                   /*
                   這2點的位置在圓心點旁邊附近。也可以設置成圓心點。
                   連接點:當手落在當前view上時,連接線要連接的點
                   */
                       start.x = currentPosView.getLeft() / 2 + currentPosView.getRight() / 2;
                       start.y = currentPosView.getTop() / 2 + currentPosView.getBottom() / 2;
                       if (size == 0) {//表示是第一次。設置開頭點
                           linePath.moveTo(start.x, start.y);
                       } else {//連接到連接點
                           linePath.lineTo(start.x, start.y);
                       }
                   }
                   /*
                   這里一定要做。不然情況就是 只要經過了對角就會把對角一條線一起選擇
                    */
                   currentStartView = currentPosView;
               }
               end.x = x;
               end.y = y;
               break;
           case MotionEvent.ACTION_UP:

               final int size = choose.size();

               //手勢結束后,讓兩點合一,不顯示多余的尾巴
               end.x = start.x;
               end.y = start.y;

               //選擇的密碼數(shù)量與設置的密碼長度不符合
               if (size != passwdLen) {
                   for (int i = 0; i < size; i++) {
                       ((LockView) getChildAt(choose.get(i) - 1)).setStatus(LockView.ERROR);
                   }
                   if (mListener != null && size > 0) {
                       int[] a = new int[choose.size()];
                       for (int i = 0; i < size; i++) {
                           a[i] = choose.get(i);
                       }
                       mListener.onError(a);
                   }
               } else {
                   boolean isSuccess = true;

                   for (int i = 0; i < size; i++) {
                       final Integer item = choose.get(i);

                       //繪制的路徑中只要有一個不對,中判斷為錯誤
                       if (passwd.get(i).intValue() != item.intValue()) {
                           isSuccess = false;
                           break;
                       }
                   }
                   linePaint.setColor(isSuccess ? Color.parseColor("#0094ff") : Color.RED);
                   for (int i = 0; i < size; i++) {
                       final Integer item = choose.get(i);
                       ((LockView) getChildAt(item - 1)).setStatus(isSuccess ? LockView.SUCCESS : LockView.ERROR);
                   }
                   if (isSuccess) {
                       if (mListener != null) {

                           mListener.onSuccess();

                       }
                   } else {

                       if (mListener != null) {
                           int[] a = new int[choose.size()];
                           for (int i = 0; i < size; i++) {
                               a[i] = choose.get(i);
                           }
                           mListener.onError(a);
                       }
                   }
               }

               if (mListener != null ) {
                   int[] a = new int[choose.size()];
                   for (int i = 0; i < size; i++) {
                       a[i] = choose.get(i);
                   }
                   mListener.onComplete(a);
               }
               break;
       }
       invalidate();
       return true;
   }

   /**
    * 根據(jù)坐標值,獲得view
    *
    * @param x
    * @param y
    * @return
    */
   private LockView getChildByPos(int x, int y) {

       final int childCount = getChildCount();
       for (int i = 0; i < childCount; i++) {
           final LockView child = (LockView) getChildAt(i);

           //x在當前view的寬度范圍內,y在當前view的高度范圍內

           if ((x >= child.getLeft()) && (x <= child.getRight())
                   && (y >= child.getTop()) && (y <= child.getBottom())) {

               final int radiusX = child.getMeasuredWidth() / 2;
               final int radiusY = child.getMeasuredHeight() / 2;
               int centerX = child.getRight() - radiusX;
               int centerY = child.getBottom() - radiusY;
               /*
                求點是否在圓上。公式:
                   到圓心的距離 是否大于半徑。半徑是R  如O(x,y)點圓心,任意一點P(x1,y1) (x-x1)*(x-x1)+(y-y1)*(y-y1)>R*R 那么在圓外 反之在圓內

                    手指經過當前view時,如果是在圓內,則判定有效,如果是在當前view的四邊角但是沒有在圓的范圍內,則無效
                */

               if (!((centerX - x) * (centerX - x) + (centerY - y) * (centerY - y) > radiusX * radiusX)) {

                   return child;
               }

           }
       }
       return null;
   }

   /**
    * 計算兩個view之間是否有需要連接的中間view.<br/>
    * eg:如果點的3,拖到7,成對角,那么中間需要自動連接的就是5
    *
    * @param start
    * @param end
    * @return 返回中間view在父view的位置。沒有就返回-1
    */
   private int getBetween(LockView start, LockView end) {
       int s = getPosByView(start);
       int e = getPosByView(end);
       if (s == -1 || e == -1) {
           return -1;
       }
       Integer betweenPos = getBetweenPosByKey("" + s + "," + e);

       return betweenPos == null ? -1 : betweenPos;
   }

   /**
    * 根據(jù)key,查找中間值。<br/>
    * eg:key="3,7",value=5
    *
    * @param key
    * @return value
    */
   private Integer getBetweenPosByKey(String key) {
       return betweenMap.get(key);
   }

   /**
    * 根據(jù)view計算其在父view中所在位置
    *
    * @param view
    * @return 沒有返回-1
    */
   private int getPosByView(LockView view) {
       final int childCount = getChildCount();
       for (int i = 0; i < childCount; i++) {
           if (getChildAt(i) == view) {
               return (i + 1);
           }
       }
       return -1;
   }

   private Paint linePaint = new Paint();
   private Point start = new Point();
   private Point end = new Point();

   @Override
   protected void dispatchDraw(Canvas canvas) {
       super.dispatchDraw(canvas);

       linePaint.setStyle(Paint.Style.STROKE);
       linePaint.setStrokeWidth(4);

       //繪制連接線
       canvas.drawPath(linePath, linePaint);

       if (choose.size() > 0) {
           if (end.x != 0 && end.y != 0) {

               //這個主要是當手勢落在某個view上時,能自動把點定位連接到view的連接點
               canvas.drawLine(start.x, start.y, end.x, end.y, linePaint);
           }
       }
   }

   /**
    * 設置密碼
    *
    * @param pwd
    */
   public void setPasswd(int[] pwd) {
       this.passwdLen = pwd.length;
       this.passwd.clear();
       for (int i = 0; i < this.passwdLen; i++) {
           this.passwd.add(pwd[i]);
       }

   }

   @Override
   protected LayoutParams generateLayoutParams(LayoutParams p) {
       return new MarginLayoutParams(p);
   }

   public void setListener(LockListener listener) {
       mListener = listener;
   }

   public interface LockListener {
       void onComplete(int[] a);
       void onSuccess();
       void onError(int[] integers);
   }
}

頂部的指示器

/**
* 主要就是 一個9位的數(shù)組。如果不設置密碼的時候,全部都是0
* 如果某位設置了密碼。則把數(shù)組對應的位置值設置1
* eg: 沒設置密碼時
* passwd的內容 0 0 0 0 0 0 0 0 0
* 設置密碼32147
* passwd的內容 1 1 1 1 0 0 1 0 0
* 最后在繪制的時候根據(jù)passwd的每個值0或1來繪制實心或空心圓。
* 因為只是做頂部的展示用,沒有其它,所以就這么來
*/
public class MiniLockView extends View {
  private Paint mPaint;
  private final byte[] passwd = new byte[9];
  private int mColNum;

  public MiniLockView(Context context, AttributeSet attrs) {
      super(context, attrs);

      mPaint = new Paint();
      mColNum = 3;
      Arrays.fill(passwd, (byte) 0);
  }


  @Override
  protected void onDraw(Canvas canvas) {
      super.onDraw(canvas);
      int pwdColor = Color.parseColor("#0094ff");

      int radius =20;
      int count = passwd.length;
      for (int i = 0; i < count; i++) {
          int row = i / mColNum;
          int col = i % mColNum;
          final byte b = passwd[i];
          //如果當前位為0,則表示譔位不是密碼位
          //如果為1,表示是密碼位
          if (b == 0) {
              mPaint.setStyle(Paint.Style.STROKE);
              mPaint.setColor(Color.LTGRAY);
          } else {
              mPaint.setStyle(Paint.Style.FILL);
              mPaint.setColor(pwdColor);
          }
          int x= (radius * 2+radius) * (col+1 );
          int y = (radius * 2+radius) * (row+1 );
          canvas.drawCircle(x,y , radius, mPaint);
      }

  }

  /**
   * 設置密碼
   * @param pwd
   */
  public void setPasswd(int[] pwd) {
      Arrays.fill(passwd, (byte) 0);
      final int length = pwd.length;
      for (int i = 0; i < length; i++) {
          final int index = pwd[i] - 1;
          this.passwd[index] = 1;
      }
      invalidate();
  }
}

布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

              xmlns:tools="http://schemas.android.com/tools"
              android:background="#000000"
                android:gravity="center"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

<com.example.xp.mycustomviewapplication.MiniLockView

android:id="@+id/mini_lock_view"
    android:layout_width="100dp"
    android:layout_height="100dp"/>

    <com.example.xp.mycustomviewapplication.GestureLockView
        android:id="@+id/lock_view"

        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

MainActivity的oncreate

     final GestureLockView gestureLockView = (GestureLockView) findViewById(R.id.lock_view);
        final MiniLockView miniLockView = (MiniLockView) findViewById(R.id.mini_lock_view);
         int[] a =new int[]{2, 6, 9, 8, 7, 4, 2};
        gestureLockView.setPasswd(a);
        gestureLockView.setListener(new GestureLockView.LockListener() {
            @Override
            public void onComplete(int[] a) {
                if (a.length !=0)
                  Toast.makeText(MainActivity.this, "onComplete:"+ Arrays.toString(a), Toast.LENGTH_SHORT).show();
                miniLockView.setPasswd(a);
            }

            @Override
            public void onSuccess() {
            }

            @Override
            public void onError(int[] integers) {
            }
        });

寫在最后

本人也是一個自定義view的low,學習中,代碼寫得不是很嚴謹,還有些小東西沒做處理。主要是學習一下思路。其它的在基于項目中的需求在修改下就好了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,836評論 25 709
  • 6、View的繪制 (1)當測量好一個View之后,我們就可以簡單的重寫 onDraw()方法,并在 Canvas...
    b5e7a6386c84閱讀 1,980評論 0 3
  • BroadcastReceiver作為Android四大組件之一,即廣播。廣播分為發(fā)送者和接收者。要想使用廣播,首...
    johnnycmj閱讀 2,971評論 0 0
  • 想象之中,雨過一段彩虹;抬起了頭,瑟瑟灰色天空。 想象之中,付出會有結果;毫無保留,信奉你的承諾。 給我一首歌的時...
    曉時明玥閱讀 398評論 0 0
  • 今天上午的時候,朋友打電話過來說,讓我給我老公買份保險,其實以前就有說過,最開始的時候是準備給我老公買的,可是當時...
    月兒的2016閱讀 314評論 0 0

友情鏈接更多精彩內容