您好,登錄后才能下訂單哦!
最近項目上用到一個密碼加鎖功能,需要一個數字密碼界面,就想著封裝成一個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進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。