首页 > 系统 > Android > 正文

Android自定义View实现数字密码锁

2019-10-22 18:10:10
字体:
来源:转载
供稿:网友

最近项目上用到一个密码加锁功能,需要一个数字密码界面,就想着封装成一个View来方便管理和使用。

废话不多说,先上最终效果图:

Android,View,数字密码锁

思路

整体可分为2个部分来实现,1.顶部是4个密码位的填充;2.数字键盘部分。整体可以是一个纵向LinearLayout,4个密码位用横向LinearLayout即可,键盘由于是宫格形式,因此可用GridLayout来布局。由于密码位和键盘数字都是以圆圈为背景,这里采用自定义一个圆形背景ImageView来使用。

实现

1.页面布局

首先定义一个圆形背景的ImageView,由于最终实现的效果是点击的时候要填充圆背景,非点击状态下是空心圆,因此可通过改变Paint的style来动态更改显示:

/**  * 圆形背景ImageView(设置实心或空心)  */  public class CircleImageView extends ImageView{     private Paint mPaint;    private int mWidth;    private int mHeight;     public CircleImageView(Context context) {      this(context, null);    }     public CircleImageView(Context context, AttributeSet attrs) {      this(context, attrs, 0);    }     public CircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {      super(context, attrs, defStyleAttr);      initView(context);    }     public void initView(Context context){      mPaint = new Paint();      mPaint.setStyle(Paint.Style.STROKE);      mPaint.setColor(mPanelColor);      mPaint.setStrokeWidth(mStrokeWidth);      mPaint.setAntiAlias(true);    }     @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {      super.onSizeChanged(w, h, oldw, oldh);      mWidth = w;      mHeight = h;    }     @Override    public void draw(Canvas canvas) {      canvas.drawCircle(mWidth/2, mHeight/2, mWidth/2 - 6, mPaint);      super.draw(canvas);    }     /**    * 设置圆为实心状态    */    public void setFillCircle(){      mPaint.setStyle(Paint.Style.FILL);      invalidate();    }     /**    * 设置圆为空心状态    */    public void setStrokeCircle(){      mPaint.setStyle(Paint.Style.STROKE);      invalidate();    }  } 

可以看到,在onDraw中绘制了一个圆,默认为空心状态,定义setFillCircle和setStrokeCircle这两个方法以便外界可以方便地切换圆为实心或者空心。

圆形ImageView定义好了,开始添加密码位,布局如下:

inputResultView = new LinearLayout(context); for(int i=0; i<4; i++){    CircleImageView mResultItem = new CircleImageView(context);    mResultIvList.add(mResultItem);    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mResultIvRadius, mResultIvRadius);    params.leftMargin = dip2px(context, 4);    params.rightMargin = dip2px(context, 4);    mResultItem.setPadding(dip2px(context, 2),dip2px(context, 2),dip2px(context, 2),dip2px(context, 2));    mResultItem.setLayoutParams(params);    inputResultView.addView(mResultItem); } LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); params.gravity = Gravity.CENTER_HORIZONTAL; params.bottomMargin = dip2px(context, 34); inputResultView.setLayoutParams(params); addView(inputResultView); 

接着添加数字键盘部分的布局:

GridLayout numContainer = new GridLayout(context); numContainer.setColumnCount(3); for(int i=0; i<numArr.length; i++){   RelativeLayout numItem = new RelativeLayout(context);   numItem.setPadding(mPaddingLeftRight,mPaddingTopBottom,mPaddingLeftRight,mPaddingTopBottom);   RelativeLayout.LayoutParams gridItemParams = new RelativeLayout.LayoutParams(mNumRadius, mNumRadius);   gridItemParams.addRule(CENTER_IN_PARENT);   final TextView numTv = new TextView(context);   numTv.setText(numArr[i]);   numTv.setTextColor(mPanelColor);   numTv.setTextSize(30);   numTv.setGravity(Gravity.CENTER);   numTv.setLayoutParams(gridItemParams);   final CircleImageView numBgIv = new CircleImageView(context);   numBgIv.setLayoutParams(gridItemParams);   numItem.addView(numBgIv);   numItem.addView(numTv);   numContainer.addView(numItem);   if(i == 9){     numItem.setVisibility(INVISIBLE);   } }  //删除按钮 RelativeLayout deleteItem = new RelativeLayout(context); deleteItem.setPadding(mPaddingLeftRight,mPaddingTopBottom,mPaddingLeftRight,mPaddingTopBottom); RelativeLayout.LayoutParams gridItemParams = new RelativeLayout.LayoutParams(mNumRadius, mNumRadius); gridItemParams.addRule(CENTER_IN_PARENT); //假如删除按钮是设置自定义图片资源的话,可用注释这段 //ImageView deleteIv = new ImageView(context); //deleteIv.setImageResource(R.drawable.icn_delete_pw); //deleteIv.setLayoutParams(gridItemParams); //deleteItem.addView(deleteIv); TextView deleteTv = new TextView(context); deleteTv.setText("Delete"); deleteTv.setTextColor(mPanelColor); deleteTv.setTextSize(dip2px(context, 8)); deleteTv.setLayoutParams(gridItemParams); deleteTv.setGravity(Gravity.CENTER); deleteItem.addView(deleteTv); numContainer.addView(deleteItem);  LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); gridParams.gravity = Gravity.CENTER_HORIZONTAL; numContainer.setLayoutParams(gridParams); addView(numContainer); 

数字键盘这里用一个数组存数字内容,遍历添加,注意此处由于第10个的子View的时候是空白的,所以当遍历到第10个元素的时候,可以将其隐藏。遍历完后再单独添加删除按钮。

2.输入逻辑

页面布局完成了,接下来就是密码输入的逻辑部分,最终的效果是每点击一次数字,密码位就填充一个,每点击删除按钮一次,密码位就回退一个,输入4个数字之后,即完成输入,获取结果,并重置密码位。这里用一个StringBuilder变量来记录当前已输入的密码,每次添加就append进去,每次删除就调用deleteCharAt。

由于点击数字按下的时候填充,松开的时候为空心状态,所以可以在ACTION_DOWN和ACTION_UP事件中分别操作:

numTv.setOnTouchListener(new OnTouchListener() {     @Override     public boolean onTouch(View v, MotionEvent event) {       switch (event.getAction()){         case MotionEvent.ACTION_DOWN:           numBgIv.setFillCircle();           numTv.setTextColor(Color.WHITE);           if(mPassWord.length() < 4){             mPassWord.append(numTv.getText());             mResultIvList.get(mPassWord.length()-1).setFillCircle();             if(mInputListener!=null && mPassWord.length() == 4){               //已完整输入4个             }           }           break;         case MotionEvent.ACTION_UP:           numBgIv.setStrokeCircle();           numTv.setTextColor(mPanelColor);           break;       }       return true;     }  }); 

每次点击的时候,判断当前已输入的密码位是否已经超过4位,如果没超过,就继续追加。如果等于4,就说明输入完成,此时的mPassWord的内容就是最终的密码,可以用一个接口将其回调出去方便Activity中获取输入的密码:

/**  * 监听输入完毕的接口  */ private InputListener mInputListener;  public void setInputListener(InputListener mInputListener) {   his.mInputListener = mInputListener; }  public interface InputListener{   void inputFinish(String result); } 

然后在上面的ACTION_DOWN中输入数字等于4的时候,回调该接口:

if(mInputListener!=null && mPassWord.length() == 4){    mInputListener.inputFinish(mPassWord.toString()); } 

另外,删除的操作单独封装为一个方法:

/**  * 删除  */ public void delete(){   if(mPassWord.length() == 0){     return;   }   mResultIvList.get(mPassWord.length()-1).setStrokeCircle();   mPassWord.deleteCharAt(mPassWord.length()-1); } 

注意点:当前无输入密码时,直接return不作任何操作,假如已有输入数字,就删除最尾部的那个数字。

最后,还要考虑一种情况,即用户输入密码错误时的一些反馈,参照平时的习惯,一般是4个密码位左右摆动并且手机震动效果,震动结束之后,当前存储的密码位重置为初始状态,如下:

/**  * 输入错误的状态显示(包括震动,密码位左右摇摆效果,重置密码位)  */ public void showErrorStatus(){   mVibrator.vibrate(new long[]{100,100,100,100},-1);   List<Animator> animators = new ArrayList<>();   ObjectAnimator translationXAnim = ObjectAnimator.ofFloat(inputResultView, "translationX", -50.0f,50.0f,-50.0f,0.0f);   translationXAnim.setDuration(400);   animators.add(translationXAnim);   AnimatorSet btnSexAnimatorSet = new AnimatorSet();   btnSexAnimatorSet.playTogether(animators);   btnSexAnimatorSet.start();   btnSexAnimatorSet.addListener(new Animator.AnimatorListener() {     @Override     public void onAnimationStart(Animator animation) {      }      @Override     public void onAnimationEnd(Animator animation) {       resetResult();     }      @Override     public void onAnimationCancel(Animator animation) {      }      @Override     public void onAnimationRepeat(Animator animation) {      }   }); } 

可以看到,在onAnimationEnd中调用了resetResult,即动画结束时重置密码,resetResult方法如下:

/**  * 重置密码输入  */ public void resetResult(){   for(int i=0; i<mResultIvList.size(); i++){     mResultIvList.get(i).setStrokeCircle();   }   mPassWord.delete(0, 4); } 

遍历所有密码位View设置为空心,并且删除当前mPassWord变量存储的所有内容。

完整代码

完整的自定义数字密码锁代码如下:

 

package com.example.zjyang.viewtest.view;  import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.app.Service; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Vibrator; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.GridLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView;  import java.util.ArrayList; import java.util.List;  import static android.widget.RelativeLayout.CENTER_HORIZONTAL; import static android.widget.RelativeLayout.CENTER_IN_PARENT;  /**  * Created by IT_ZJYANG on 2018/1/22.  * 数字解锁键盘View  */  public class NumLockPanel extends LinearLayout {    private String[] numArr = new String[]{"1","2","3","4","5","6","7","8","9", "", "0"};    private int mPaddingLeftRight;   private int mPaddingTopBottom;   //4个密码位ImageView   private ArrayList<CircleImageView> mResultIvList;    private LinearLayout inputResultView;   //存储当前输入内容   private StringBuilder mPassWord;   //振动效果   private Vibrator mVibrator;   //整个键盘的颜色   private int mPanelColor;   //4个密码位的宽度   private int mResultIvRadius;   //数字键盘的每个圆的宽度   private int mNumRadius;   //每个圆的边界宽度   private int mStrokeWidth;    public NumLockPanel(Context context) {     this(context, null);   }    public NumLockPanel(Context context, AttributeSet attrs) {     this(context, attrs, 0);   }    public NumLockPanel(Context context, AttributeSet attrs, int defStyleAttr) {     super(context, attrs, defStyleAttr);     mPaddingLeftRight = dip2px(context, 21);     mPaddingTopBottom = dip2px(context, 10);     mPanelColor = Color.BLACK; //颜色代码可采用Color.parse("#000000");     mResultIvRadius = dip2px(context, 20);     mNumRadius = dip2px(context, 66);     mStrokeWidth = dip2px(context, 2);     mVibrator = (Vibrator)context.getSystemService(Service.VIBRATOR_SERVICE);     mResultIvList = new ArrayList<>();     mPassWord = new StringBuilder();     setOrientation(VERTICAL);     setGravity(CENTER_HORIZONTAL);     initView(context);   }    public void initView(Context context){     //4个结果号码     inputResultView = new LinearLayout(context);     for(int i=0; i<4; i++){       CircleImageView mResultItem = new CircleImageView(context);       mResultIvList.add(mResultItem);       LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mResultIvRadius, mResultIvRadius);       params.leftMargin = dip2px(context, 4);       params.rightMargin = dip2px(context, 4);       mResultItem.setPadding(dip2px(context, 2),dip2px(context, 2),dip2px(context, 2),dip2px(context, 2));       mResultItem.setLayoutParams(params);       inputResultView.addView(mResultItem);     }     LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);     params.gravity = Gravity.CENTER_HORIZONTAL;     params.bottomMargin = dip2px(context, 34);     inputResultView.setLayoutParams(params);     addView(inputResultView);      //数字键盘     GridLayout numContainer = new GridLayout(context);     numContainer.setColumnCount(3);     for(int i=0; i<numArr.length; i++){       RelativeLayout numItem = new RelativeLayout(context);       numItem.setPadding(mPaddingLeftRight,mPaddingTopBottom,mPaddingLeftRight,mPaddingTopBottom);       RelativeLayout.LayoutParams gridItemParams = new RelativeLayout.LayoutParams(mNumRadius, mNumRadius);       gridItemParams.addRule(CENTER_IN_PARENT);       final TextView numTv = new TextView(context);       numTv.setText(numArr[i]);       numTv.setTextColor(mPanelColor);       numTv.setTextSize(30);       numTv.setGravity(Gravity.CENTER);       numTv.setLayoutParams(gridItemParams);       final CircleImageView numBgIv = new CircleImageView(context);       numBgIv.setLayoutParams(gridItemParams);       numTv.setOnTouchListener(new OnTouchListener() {         @Override         public boolean onTouch(View v, MotionEvent event) {           switch (event.getAction()){             case MotionEvent.ACTION_DOWN:               numBgIv.setFillCircle();               numTv.setTextColor(Color.WHITE);               if(mPassWord.length() < 4){                 mPassWord.append(numTv.getText());                 mResultIvList.get(mPassWord.length()-1).setFillCircle();                 if(mInputListener!=null && mPassWord.length() == 4){                   mInputListener.inputFinish(mPassWord.toString());                 }               }               break;             case MotionEvent.ACTION_UP:               numBgIv.setStrokeCircle();               numTv.setTextColor(mPanelColor);               break;           }           return true;         }       });        numItem.addView(numBgIv);       numItem.addView(numTv);       numContainer.addView(numItem);       if(i == 9){         numItem.setVisibility(INVISIBLE);       }     }      //删除按钮     RelativeLayout deleteItem = new RelativeLayout(context);     deleteItem.setPadding(mPaddingLeftRight,mPaddingTopBottom,mPaddingLeftRight,mPaddingTopBottom);     RelativeLayout.LayoutParams gridItemParams = new RelativeLayout.LayoutParams(mNumRadius, mNumRadius);     gridItemParams.addRule(CENTER_IN_PARENT);     //假如删除按钮是设置自定义图片资源的话,可用注释这段     //ImageView deleteIv = new ImageView(context);     //deleteIv.setImageResource(R.drawable.icn_delete_pw);     //deleteIv.setLayoutParams(gridItemParams);     //deleteItem.addView(deleteIv);     TextView deleteTv = new TextView(context);     deleteTv.setText("Delete");     deleteTv.setTextColor(mPanelColor);     deleteTv.setTextSize(dip2px(context, 8));     deleteTv.setLayoutParams(gridItemParams);     deleteTv.setGravity(Gravity.CENTER);     deleteItem.addView(deleteTv);     numContainer.addView(deleteItem);     deleteTv.setOnClickListener(new OnClickListener() {       @Override       public void onClick(View v) {         delete();       }     });      LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);     gridParams.gravity = Gravity.CENTER_HORIZONTAL;     numContainer.setLayoutParams(gridParams);     addView(numContainer);   }    /**    * 输入错误的状态显示(包括震动,密码位左右摇摆效果,重置密码位)    */   public void showErrorStatus(){     mVibrator.vibrate(new long[]{100,100,100,100},-1);     List<Animator> animators = new ArrayList<>();     ObjectAnimator translationXAnim = ObjectAnimator.ofFloat(inputResultView, "translationX", -50.0f,50.0f,-50.0f,0.0f);     translationXAnim.setDuration(400);     animators.add(translationXAnim);     AnimatorSet btnSexAnimatorSet = new AnimatorSet();     btnSexAnimatorSet.playTogether(animators);     btnSexAnimatorSet.start();     btnSexAnimatorSet.addListener(new Animator.AnimatorListener() {       @Override       public void onAnimationStart(Animator animation) {        }        @Override       public void onAnimationEnd(Animator animation) {         resetResult();       }        @Override       public void onAnimationCancel(Animator animation) {        }        @Override       public void onAnimationRepeat(Animator animation) {        }     });   }    /**    * 删除    */   public void delete(){     if(mPassWord.length() == 0){       return;     }     mResultIvList.get(mPassWord.length()-1).setStrokeCircle();     mPassWord.deleteCharAt(mPassWord.length()-1);   }    /**    * 重置密码输入    */   public void resetResult(){     for(int i=0; i<mResultIvList.size(); i++){       mResultIvList.get(i).setStrokeCircle();     }     mPassWord.delete(0, 4);   }    /**    * 监听输入完毕的接口    */   private InputListener mInputListener;    public void setInputListener(InputListener mInputListener) {     this.mInputListener = mInputListener;   }    public interface InputListener{     void inputFinish(String result);   }     /**    * dip/dp转像素    *    * @param dipValue    *      dip或 dp大小    * @return 像素值    */   public static int dip2px(Context context, float dipValue) {     DisplayMetrics metrics = context.getResources().getDisplayMetrics();     return (int) (dipValue * (metrics.density) + 0.5f);   }    /**    * 圆形背景ImageView(设置实心或空心)    */   public class CircleImageView extends ImageView{      private Paint mPaint;     private int mWidth;     private int mHeight;      public CircleImageView(Context context) {       this(context, null);     }      public CircleImageView(Context context, AttributeSet attrs) {       this(context, attrs, 0);     }      public CircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {       super(context, attrs, defStyleAttr);       initView(context);     }      public void initView(Context context){       mPaint = new Paint();       mPaint.setStyle(Paint.Style.STROKE);       mPaint.setColor(mPanelColor);       mPaint.setStrokeWidth(mStrokeWidth);       mPaint.setAntiAlias(true);     }      @Override     protected void onSizeChanged(int w, int h, int oldw, int oldh) {       super.onSizeChanged(w, h, oldw, oldh);       mWidth = w;       mHeight = h;     }      @Override     public void draw(Canvas canvas) {       canvas.drawCircle(mWidth/2, mHeight/2, mWidth/2 - 6, mPaint);       super.draw(canvas);     }      /**      * 设置圆为实心状态      */     public void setFillCircle(){       mPaint.setStyle(Paint.Style.FILL);       invalidate();     }      /**      * 设置圆为空心状态      */     public void setStrokeCircle(){       mPaint.setStyle(Paint.Style.STROKE);       invalidate();     }   } } 

使用

在Activity的布局文件中:

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"   xmlns:tools="http://schemas.android.com/tools"   android:id="@+id/activity_main"   android:layout_width="match_parent"   android:layout_height="match_parent"   android:background="#ffffff"   tools:context="com.example.zjyang.viewtest.MainActivity">    <com.example.zjyang.viewtest.view.NumLockPanel     android:id="@+id/num_lock"     android:layout_width="match_parent"     android:layout_height="wrap_content"     android:layout_marginTop="30dp">    </com.example.zjyang.viewtest.view.NumLockPanel> </RelativeLayout> 

在代码中监听输入的密码结果:

public class MainActivity extends AppCompatActivity {      private NumLockPanel mNumLockPanel;    @Override   protected void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);     setContentView(R.layout.activity_main);      mNumLockPanel = (NumLockPanel) findViewById(R.id.num_lock);     mNumLockPanel.setInputListener(new NumLockPanel.InputListener() {       @Override       public void inputFinish(String result) {         //此处result即为输入结果         Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show();         //错误效果示例         mNumLockPanel.showErrorStatus();       }     });   } } 

最后,在自定义View构造方法中初始化了圆圆和数字的颜色风格,以及空心圆的边界粗细大小,可根据需求自行更改。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持VEVB武林网。


注:相关教程知识阅读请移步到Android开发频道。
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表