Android自定义View之数字密码锁
2018-01-22 18:15
381 查看
距上次写博客已经快一年了,计划赶不上变化,种种原因加上自己的拖延症= =、 之前想好的每月一文还是没能坚持下来,趁着闲暇之余撸一篇,希望之后能够继续坚持总结的习惯。最近项目上用到一个密码加锁功能,需要一个数字密码界面,就想着封装成一个View来方便管理和使用。
废话不多说,先上最终效果图:
![](http://img.blog.csdn.net/20180122174703929?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSVRfWkpZQU5H/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
思路
整体可分为2个部分来实现,1.顶部是4个密码位的填充;2.数字键盘部分。整体可以是一个纵向LinearLayout,4个密码位用横向LinearLayout即可,键盘由于是宫格形式,因此可用GridLayout来布局。由于密码位和键盘数字都是以圆圈为背景,这里采用自定义一个圆形背景ImageView来使用。
实现
1.页面布局
首先定义一个圆形背景的ImageView,由于最终实现的效果是点击的时候要填充圆背景,非点击状态下是空心圆,因此可通过改变Paint的style来动态更改显示:
圆形ImageView定义好了,开始添加密码位,布局如下:
接着添加数字键盘部分的布局:
2.输入逻辑
页面布局完成了,接下来就是密码输入的逻辑部分,最终的效果是每点击一次数字,密码位就填充一个,每点击删除按钮一次,密码位就回退一个,输入4个数字之后,即完成输入,获取结果,并重置密码位。这里用一个StringBuilder变量来记录当前已输入的密码,每次添加就append进去,每次删除就调用deleteCharAt。
由于点击数字按下的时候填充,松开的时候为空心状态,所以可以在ACTION_DOWN和ACTION_UP事件中分别操作:
最后,还要考虑一种情况,即用户输入密码错误时的一些反馈,参照平时的习惯,一般是4个密码位左右摆动并且手机震动效果,震动结束之后,当前存储的密码位重置为初始状态,如下:
完整代码
完整的自定义数字密码锁代码如下:
使用
在Activity的布局文件中:
最后,在自定义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实现数字密码锁
- (转)[原] Android 自定义View 密码框 例子
- Android自定义View之组合控件 ---- LED数字时钟
- 我的Android进阶之旅------>Android自定义View实现带数字的进度条(NumberProgressBar)
- Android开发自定义View实现数字与图片无缝切换的2048
- Android自定义View:圆环带数字百分比的进度条
- Android自定义View之酷炫数字圆环
- Android自定义View仿支付宝输入六位密码功能
- Android自定义View实现带数字的进度条实例代码
- Android自定义数字密码键盘
- Android自定义View实现带数字的进度条(NumberProgressBar)
- Android自定义View数字圆环(一)
- Android 自定义View和ViewGroup实现密码图案
- [原] Android 自定义View 密码框 例子
- Android 自定义带数字的圆形进度条和中间是文字的圆形进度条View
- 安卓/Android 模仿支付宝/微信 支付密码输入框的自定义View
- Android自定义View模仿密码输入框
- Android自定义View之数字时钟
- Android自定义View数字圆环(二)
- Android自定义View之六位密码框