Android自定义View之数字密码锁

距上次写博客已经快一年了,计划赶不上变化,种种原因加上自己的拖延症= =、 之前想好的每月一文还是没能坚持下来,趁着闲暇之余撸一篇,希望之后能够继续坚持总结的习惯。最近项目上用到一个密码加锁功能,需要一个数字密码界面,就想着封装成一个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中绘制了一个圆,默认为空心状态,定义 setFillCirclesetStrokeCircle这两个方法以便外界可以方便地切换圆为实心或者空心。

圆形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_DOWNACTION_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构造方法中初始化了圆圆和数字的颜色风格,以及空心圆的边界粗细大小,可根据需求自行更改。







  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值