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中绘制了一个圆,默认为空心状态,定义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构造方法中初始化了圆圆和数字的颜色风格,以及空心圆的边界粗细大小,可根据需求自行更改。

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

以上是 Android自定义View实现数字密码锁 的全部内容, 来源链接: utcz.com/p/240637.html

回到顶部