最近項目上用到一個密碼加鎖功能,需要一個數字密碼界面,就想著封裝成一個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構造方法中初始化了圓圓和數字的顏色風格,以及空心圓的邊界粗細大小,可根據需求自行更改。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。