有一個(gè)場景,需要輸入短信驗(yàn)證碼。So,嘗試著自己設(shè)計(jì)了一個(gè)這樣的View。參考了一些App,發(fā)現(xiàn)建設(shè)銀行手機(jī)銀行的短信驗(yàn)證碼界面是我想要的。所以,設(shè)計(jì)了如下圖這樣兩個(gè)短信輸入框原型。
本頁圖稍微有點(diǎn)大,可能要加載一會兒。

再看一個(gè)最終的效果圖。

特點(diǎn)
隨輸入的字符產(chǎn)生動畫效果(如上圖)
額,當(dāng)然,圖有點(diǎn)糊了,看的不是很清楚。
分兩個(gè)場景,輸入和刪除
輸入
當(dāng)用戶輸入一個(gè)數(shù)字的時(shí)大概有兩個(gè)效果:
- 文字alpha由全透明變成不透明
- 指示底線從中間向兩邊發(fā)生顏色漸變
刪除
當(dāng)用戶刪除一個(gè)數(shù)字的時(shí)大概有兩個(gè)效果:
- 文字alpha由不透明變成透明(消失)
- 指示底線從兩邊向中間發(fā)生顏色漸變
如何實(shí)現(xiàn)?
寫代碼重要的是分解。所以,看上面的原型,我們可以這樣分解:一個(gè)ViewGroup承載著幾個(gè)View。ViewGroup水平布局著這些VIew。每一個(gè)View在顯示和消失時(shí),會有一個(gè)動畫。如果這樣分解的話,我們就很清楚了如何來實(shí)現(xiàn)這個(gè)效果了。
Show you the code. 代碼由一個(gè)ViewGroup和一個(gè)View構(gòu)成。
①. SingleNumberView(View)
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;
/**
* 功能說明:<br>
* <ul>
* <li>當(dāng)用戶輸入文字之后,產(chǎn)生兩個(gè)動畫:
* <ol>
* <li>文字透明度變化:文字透明度由透明度100%到0%</li>
* <li>底部標(biāo)線顏色變化:底部標(biāo)線激活顏色由水平中心擴(kuò)展到兩端</li>
* </ol>
* </li>
* <p>
* <li>當(dāng)用戶清除了文字之后,產(chǎn)生兩個(gè)動畫:
* <ol>
* <li>文字透明度變化:文字透明度由透明度0%到100%</li>
* <li>底部標(biāo)線顏色變化:底部標(biāo)線激活顏色由兩端收縮到中心,然后不可見</li>
* </ol>
* </li>
* </ul>
*/
public class SingleNumberView extends View {
private static final String TAG = SingleNumberView.class.getSimpleName();
/**
* 相關(guān)動畫:文字顏色動畫、底部標(biāo)線動畫
*/
private Animation lineExpenseAnimation;
private Animation lineShrinkAnimation;
/**
* 動畫周期 單位:ms
*/
private int mDuration = 500;
/**
* 動畫百分比(不是動畫消逝時(shí)間百分比) InterpolatorFraction
*/
private float mInterpolatorFraction = 0;
/**
* 當(dāng)前數(shù)字
*/
private String mNumber = "";
/**
* 文本顏色
*/
private int textColor = Color.BLACK;
/**
* 文本字體大小
*/
private int textSize = (int) (Resources.getSystem().getDisplayMetrics().density * 25);
/**
* 文本為空底部文字顏色
*/
private int mBottomLineEmptyColor = Color.parseColor("#47b4db");
/**
* 文本為激活狀態(tài)文字顏色
*/
private int mBottomLineActiveColor = Color.parseColor("#6ae1ff");
/**
* 底部線的寬窄
*/
private int mBottomLineWidth = (int) (Resources.getSystem().getDisplayMetrics().density * 1.5);
/**
* 文本畫筆
*/
private Paint mTextPaint;
/**
* 標(biāo)線畫筆
*/
private Paint mBottomLinePaint;
public SingleNumberView(Context context) {
super(context);
init();
}
public SingleNumberView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public void init() {
//初始化動畫對象
lineExpenseAnimation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
ensureInterpolator();
mInterpolatorFraction = getInterpolator().getInterpolation(interpolatedTime);
// Log.e("SingleNumberView", mInterpolatorFraction + " .");
mTextPaint.setAlpha((int) (mInterpolatorFraction * 255));
invalidate();
}
};
lineExpenseAnimation.setDuration(mDuration);
lineShrinkAnimation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
ensureInterpolator();
mInterpolatorFraction = getInterpolator().getInterpolation(1 - interpolatedTime);
// Log.e("SingleNumberView", mInterpolatorFraction + " ;");
mTextPaint.setAlpha((int) (mInterpolatorFraction * 255));
invalidate();
}
};
lineShrinkAnimation.setDuration(mDuration);
lineShrinkAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
// mNumber = "";
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
//初始化畫筆
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(textSize);
mTextPaint.setColor(textColor);
mBottomLinePaint = new Paint();
mBottomLinePaint.setStrokeCap(Paint.Cap.ROUND);
mBottomLinePaint.setStrokeWidth(mBottomLineWidth);
}
/**
* 開始繪制
*/
public void onDraw(Canvas canvas) {
//開始繪制文字
if (!TextUtils.isEmpty(mNumber)) {
//繪制文字
//仔細(xì)推導(dǎo)一下,就會找到合適的居中工具(可參考引文書寫四線三格)
int baseline = getTextBaseline(getPaddingTop());
canvas.drawText(mNumber, getPaddingLeft() + mTextPaint.measureText("8") / 4, baseline, mTextPaint);
} else {
//不需要繪制文字
}
//開始繪制底部基礎(chǔ)線框
int lineY = (int) (getMeasuredHeight() - mBottomLinePaint.getStrokeWidth() - getPaddingBottom());
int lineLength = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
int lineStart = getPaddingLeft();
mBottomLinePaint.setColor(mBottomLineEmptyColor);
canvas.drawLine(lineStart,
lineY,
lineStart + lineLength,
lineY, mBottomLinePaint);
//開始繪制底部激活線框
mBottomLinePaint.setColor(mBottomLineActiveColor);
lineLength = (int) (mInterpolatorFraction * (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()));
lineStart = (getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - lineLength) / 2 + getPaddingLeft();
if (lineLength > 0f && lineStart > 0f) {
canvas.drawLine(lineStart,
lineY,
lineStart + lineLength,
lineY, mBottomLinePaint);
}
}
private int getTextBaseline(int top) {
Rect bounds = new Rect();
mTextPaint.getTextBounds(mNumber, 0, mNumber.length(), bounds);
Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
int center = top + bounds.height() / 2;
int baseline = center + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
// Log.e(TAG, "baseline = " + baseline);
return baseline;
}
public void setTextColor(int textColor) {
this.textColor = textColor;
mTextPaint.setColor(textColor);
}
public void setTextSize(int textSize) {
this.textSize = textSize;
mTextPaint.setTextSize(textSize);
}
public void setActiveColor(int color) {
mBottomLineActiveColor = color;
}
public void setInactiveColor(int color) {
mBottomLineEmptyColor = color;
}
public void setBottomLineWidth(int width) {
mBottomLineWidth = width;
mBottomLinePaint.setStrokeWidth(mBottomLineWidth);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = measureWidth(widthMeasureSpec);
int measureHeight = measureHeight(heightMeasureSpec);
setMeasuredDimension(measureWidth, measureHeight);
}
private int measureWidth(int pWidthMeasureSpec) {
int result = 0;
int widthMode = MeasureSpec.getMode(pWidthMeasureSpec);// 得到模式
int widthSize = MeasureSpec.getSize(pWidthMeasureSpec);// 得到尺寸
switch (widthMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
// Log.e(TAG, "我被測量啦,width :" + getPaddingLeft() + "|" + getPaddingRight());
result = (int) (mTextPaint.measureText("8") * 1.5f + getPaddingLeft() + getPaddingRight());
break;
case MeasureSpec.EXACTLY:
// match_parent或具體的值如:60dp
result = widthSize;
break;
}
return result;
}
private int measureHeight(int pHeightMeasureSpec) {
int result = 0;
int heightMode = MeasureSpec.getMode(pHeightMeasureSpec);
int heightSize = MeasureSpec.getSize(pHeightMeasureSpec);
switch (heightMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
// Log.e(TAG, "我被測量啦,height :" + getPaddingTop() + "|" + getPaddingBottom());
Rect bounds = new Rect();
mTextPaint.getTextBounds("8", 0, 1, bounds);
result = bounds.height() + getPaddingTop() + getPaddingBottom();
//線寬
result += mBottomLinePaint.getStrokeWidth();
//這個(gè)是文字與下劃線的間隔
result += getPaddingBottom();
break;
case MeasureSpec.EXACTLY:
// match_parent或具體的值如:60dp
result = heightSize;
break;
}
return result;
}
public void setNumber(String mNumber) {
if (lineShrinkAnimation != null) {
lineShrinkAnimation.cancel();
}
if (lineExpenseAnimation != null) {
lineExpenseAnimation.cancel();
}
if (TextUtils.isEmpty(mNumber)) {
startAnimation(lineShrinkAnimation);
} else {
this.mNumber = mNumber;
startAnimation(lineExpenseAnimation);
}
}
}
在這里插一句,一般我分析一個(gè)自定義View,首先會看構(gòu)造函數(shù),然后是onMeasure方法,在來onLayout方法,最后是onDraw方法。如果這個(gè)自定義View還定義了復(fù)雜的手勢交互,可能還需要看onTouchEvent。如果是ViewGroup可能還需要看看onInterceptTouchEvent。當(dāng)然,也需要看看這個(gè)View是否支持嵌套滑動。以上就是套路。
構(gòu)造函數(shù)
按照上面的套路,我們首先看看構(gòu)造函數(shù)??偣仓貙懥藘蓚€(gè)構(gòu)造函數(shù)。關(guān)于這兩個(gè)構(gòu)造函數(shù)分別是在什么時(shí)間調(diào)用,請自己百度,不在此搬運(yùn)別人的分析了。共同點(diǎn)是,兩個(gè)構(gòu)造函數(shù)都調(diào)用了一個(gè)共同的函數(shù) - init()。讓我們看看在這個(gè)方法中做了什么。
public void init() {
//初始化動畫對象
lineExpenseAnimation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
ensureInterpolator();
mInterpolatorFraction = getInterpolator().getInterpolation(interpolatedTime);
// Log.e("SingleNumberView", mInterpolatorFraction + " .");
mTextPaint.setAlpha((int) (mInterpolatorFraction * 255));
invalidate();
}
};
lineExpenseAnimation.setDuration(mDuration);
lineShrinkAnimation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
ensureInterpolator();
mInterpolatorFraction = getInterpolator().getInterpolation(1 - interpolatedTime);
// Log.e("SingleNumberView", mInterpolatorFraction + " ;");
mTextPaint.setAlpha((int) (mInterpolatorFraction * 255));
invalidate();
}
};
lineShrinkAnimation.setDuration(mDuration);
lineShrinkAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
// mNumber = "";
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
//初始化畫筆
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(textSize);
mTextPaint.setColor(textColor);
mBottomLinePaint = new Paint();
mBottomLinePaint.setStrokeCap(Paint.Cap.ROUND);
mBottomLinePaint.setStrokeWidth(mBottomLineWidth);
}
共創(chuàng)建了兩個(gè)動畫,分別完成我們在原型中設(shè)計(jì)的動效:
輸入
當(dāng)用戶輸入一個(gè)數(shù)字的時(shí)有兩個(gè)動畫效果:
- 文字alpha由全透明變成不透明
- 指示底線從中間向兩邊發(fā)生顏色漸變
刪除
當(dāng)用戶刪除一個(gè)數(shù)字的時(shí)有兩個(gè)動畫效果:
- 文字alpha由不透明變成透明(消失)
- 指示底線從兩邊向中間發(fā)生顏色漸變
兩個(gè)動畫的 applyTransformation 方法中,根據(jù)動畫消逝的時(shí)間比例,計(jì)算出mInterpolatorFraction。mInterpolatorFraction是完成動畫的關(guān)鍵因數(shù),所有的動畫效果它有關(guān)系。如,在這個(gè)方法中,緊接著就根據(jù)這個(gè)因數(shù),設(shè)置了文字畫筆mTextPaint的alpha。
此外,在init方法中,還創(chuàng)建了兩個(gè)畫筆,分別繪制數(shù)字和底部劃線。
onMeasure方法
這個(gè)方法的作用是在系統(tǒng)繪制你的自定義View之前,先測量View的大小。如何理解?就像是我們在給墻壁貼壁紙時(shí),首先要知道墻壁以及每一張壁紙的尺寸。我們就相當(dāng)于是Android系統(tǒng),墻壁就是我們的View所在的ViewGroup,View當(dāng)然就相當(dāng)于壁紙。在給墻壁貼壁紙之前,首先會測量墻壁和壁紙的尺寸(measure),然后布局(layout),最后貼圖(draw)。同樣繪制前,ViewGroup會調(diào)用我們的View的measure方法,讓自定義View測量自己。你可能會說:啥?我讀書少,你可不要騙我,我分明沒有看到那你重寫這個(gè)方法。對,你的思維很活躍。但是深度不夠。如果你足夠仔細(xì)的話,可以看到我們的自定義View是繼承于android.view.View的。你再閱讀以下View的源碼,會發(fā)現(xiàn),measure方法是final的,我們是無法繼承的。但是,看不到,并不代表沒有。在measure方法中,調(diào)用了onMeasure方法。扯了一大堆,讓我們看看代碼。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = measureWidth(widthMeasureSpec);
int measureHeight = measureHeight(heightMeasureSpec);
setMeasuredDimension(measureWidth, measureHeight);
}
private int measureWidth(int pWidthMeasureSpec) {
int result = 0;
int widthMode = MeasureSpec.getMode(pWidthMeasureSpec);// 得到模式
int widthSize = MeasureSpec.getSize(pWidthMeasureSpec);// 得到尺寸
switch (widthMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
// Log.e(TAG, "我被測量啦,width :" + getPaddingLeft() + "|" + getPaddingRight());
result = (int) (mTextPaint.measureText("8") * 1.5f + getPaddingLeft() + getPaddingRight());
break;
case MeasureSpec.EXACTLY:
// match_parent或具體的值如:60dp
result = widthSize;
break;
}
return result;
}
private int measureHeight(int pHeightMeasureSpec) {
int result = 0;
int heightMode = MeasureSpec.getMode(pHeightMeasureSpec);
int heightSize = MeasureSpec.getSize(pHeightMeasureSpec);
switch (heightMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
// Log.e(TAG, "我被測量啦,height :" + getPaddingTop() + "|" + getPaddingBottom());
Rect bounds = new Rect();
mTextPaint.getTextBounds("8", 0, 1, bounds);
result = bounds.height() + getPaddingTop() + getPaddingBottom();
//線寬
result += mBottomLinePaint.getStrokeWidth();
//這個(gè)是文字與下劃線的間隔
result += getPaddingBottom();
break;
case MeasureSpec.EXACTLY:
// match_parent或具體的值如:60dp
result = heightSize;
break;
}
return result;
}
onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法有兩個(gè)入?yún)?,分別是寬和高。每一個(gè)MeasureSpec方法都包含兩個(gè)信息,模式和尺寸。我們可以通過MeasureSpec.getMode和MeasureSpec.getSize兩個(gè)方法獲取。
有三種模式,分別是:UNSPECIFIED、EXACTLY和AT_MOST:
UNSPECIFIED:說明父ViewGroup沒有對子View強(qiáng)加任何限制,子View可以是它想要的任何尺寸。用得比較少,表示子布局被限制在一個(gè)最大值內(nèi),一般當(dāng)childView設(shè)置其寬、高為wrap_content時(shí),ViewGroup會將其設(shè)置為AT_MOST,換言之,表示子布局想要多大就多大。一般出現(xiàn)在可以滑動的ViewGroup,很好理解,屏幕不可能無限大,既然又能支持子View想要多少就能得到多少,當(dāng)然是通過滑動來實(shí)現(xiàn)的。如AadapterView的item的heightMode中、ScrollView的childView的heightMode中
EXACTLY:父ViewGroup為子View決定了一個(gè)確切的尺寸,子View將會被強(qiáng)制賦予這些邊界限制,不管子View自己想要多大(View類onMeasure方法中只支持EXACTLY),換言之,表示設(shè)置了精確的值,一般當(dāng)childView在xml或代碼中設(shè)置其寬、高為精確值、match_parent時(shí),ViewGroup會將其設(shè)置為EXACTLY,即在布局文件或代碼中可以解析指定的具體尺寸和match_parent。
AT_MOST:子View可以是自己指定的任意大小,但是有個(gè)上限。比如說當(dāng)MeasureSpec.EXACTLY的父容器為子級決定了一個(gè)大小,子級大小只能在這個(gè)父容器限制的范圍之內(nèi)。即在布局文件中可以解析wrap_content,換言之,表示子布局被限制在一個(gè)最大值內(nèi),一般當(dāng)childView設(shè)置其寬、高為wrap_content時(shí),ViewGroup會將其設(shè)置為AT_MOST。
可以看到,在 measureWidth 方法中,我們首先判斷了模式,然后根據(jù)不同的模式,給出自己的寬度值。如果是EXACTLY模式,我們就按照給定的值,給出自己的寬度。如果是UNSPECIFIED或AT_MOST模式,就設(shè)置一個(gè)數(shù)字“8”的寬度1.5倍加上左右的padding。關(guān)于高度的測量,我就不解釋了,邏輯類似。
onLayout
作為一個(gè)View,就沒有必要重寫這方法了。
onDraw
這個(gè)是視圖顯示的核心部分了。
/**
* 開始繪制
*/
public void onDraw(Canvas canvas) {
//開始繪制文字
if (!TextUtils.isEmpty(mNumber)) {
//繪制文字
//仔細(xì)推導(dǎo)一下,就會找到合適的居中工具(可參考引文書寫四線三格)
int baseline = getTextBaseline(getPaddingTop());
canvas.drawText(mNumber, getPaddingLeft() + mTextPaint.measureText("8") / 4, baseline, mTextPaint);
} else {
//不需要繪制文字
}
//開始繪制底部基礎(chǔ)線框
int lineY = (int) (getMeasuredHeight() - mBottomLinePaint.getStrokeWidth() - getPaddingBottom());
int lineLength = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
int lineStart = getPaddingLeft();
mBottomLinePaint.setColor(mBottomLineEmptyColor);
canvas.drawLine(lineStart,
lineY,
lineStart + lineLength,
lineY, mBottomLinePaint);
//開始繪制底部激活線框
mBottomLinePaint.setColor(mBottomLineActiveColor);
lineLength = (int) (mInterpolatorFraction * (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()));
lineStart = (getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - lineLength) / 2 + getPaddingLeft();
if (lineLength > 0f && lineStart > 0f) {
canvas.drawLine(lineStart,
lineY,
lineStart + lineLength,
lineY, mBottomLinePaint);
}
}
代碼這么短,你是不是很失望?ha ha ha,濃縮才能成為精華。
這個(gè)方法,其實(shí)就做了兩件事:
- 繪制文字
- 繪制底部標(biāo)線
- 基礎(chǔ)標(biāo)線
- 激活標(biāo)線
使用了canvas一些常見的方法,很簡單。沒有用過的同學(xué),可以查看API Reference。
看到這個(gè)方法,你是否還在困惑動畫是如何實(shí)現(xiàn)的呢?請注意一下,我們剛剛在將構(gòu)造函數(shù)時(shí),提到了init方法中的 mInterpolatorFraction 變量。這個(gè)變量一直被動畫改變,在這個(gè)變量被改變之后,invalidate 方法接著被調(diào)用,地球人都知道的是:這個(gè)方法會導(dǎo)致View重新繪制。這意味著onDraw方法接著會被調(diào)用。而我們在繪制底部激活線時(shí),又是根據(jù) mInterpolatorFraction 來控制線的長短。就這樣,產(chǎn)生了動畫。簡單不簡單,可愛不可愛。
②. NumberInputView(ViewGroup)
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.LinearLayout;
import com.jaesoon.messageverifydemo.R;
import java.util.ArrayList;
public class NumberInputView extends LinearLayout {
private String TAG = "NumberInputView";
private InputMethodManager input;//輸入法管理
private ArrayList<Integer> result;//輸入結(jié)果保存
private int digit = 6;//密碼位數(shù)
private int mActiveColor = Color.parseColor("#6ae1ff");
private int mInactiveColor = Color.parseColor("#47b4db");
private int mTextColor = Color.parseColor("#000000");
private int mTextSize = (int) (Resources.getSystem().getDisplayMetrics().density * 25);
private int mSpacing = (int) (Resources.getSystem().getDisplayMetrics().density * 4);
private int mBottomLineWidth = (int) (Resources.getSystem().getDisplayMetrics().density * 1.5);
public NumberInputView(Context context) {
super(context);
init(context, null);
}
public NumberInputView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
this.setFocusable(true);
this.setFocusableInTouchMode(true);
clearFocus();
input = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
result = new ArrayList<>();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumberInputView);
int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.NumberInputView_activeColor:
mActiveColor = a.getColor(attr, mActiveColor);
break;
case R.styleable.NumberInputView_inactiveColor:
mInactiveColor = a.getColor(attr, mInactiveColor);
break;
case R.styleable.NumberInputView_numberColor:
mTextColor = a.getColor(attr, mTextColor);
break;
case R.styleable.NumberInputView_numberTextSize:
mTextSize = a.getDimensionPixelSize(attr, mTextSize);
break;
case R.styleable.NumberInputView_spacing:
mSpacing = a.getDimensionPixelSize(attr, mSpacing);
break;
case R.styleable.NumberInputView_bottomLineWidth:
mBottomLineWidth = a.getDimensionPixelSize(attr, mBottomLineWidth);
break;
case R.styleable.NumberInputView_digit:
digit = a.getInt(attr, digit);
break;
}
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (getChildCount() <= 0) {
for (int i = 0; i < 6; i++) {
SingleNumberView singleNumberView = new SingleNumberView(getContext(), null);
singleNumberView.setPadding(mSpacing / 2, mSpacing / 2, mSpacing / 2, mSpacing);
singleNumberView.setTextColor(mTextColor);
singleNumberView.setTextSize(mTextSize);
singleNumberView.setActiveColor(mActiveColor);
singleNumberView.setInactiveColor(mInactiveColor);
singleNumberView.setBottomLineWidth(mBottomLineWidth);
LinearLayout.LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
singleNumberView.setLayoutParams(layoutParams);
addView(singleNumberView);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {//點(diǎn)擊控件彈出輸入鍵盤
requestFocus();
input.showSoftInput(this, InputMethodManager.SHOW_FORCED);
return true;
}
return super.onTouchEvent(event);
}
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
if (gainFocus) {
input.showSoftInput(this, InputMethodManager.SHOW_FORCED);
} else {
input.hideSoftInputFromInputMethod(this.getWindowToken(), 0);
}
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hasWindowFocus) {
input.hideSoftInputFromWindow(this.getWindowToken(), 0);
}
}
public String getText() {
StringBuffer sb = new StringBuffer();
for (int i : result) {
sb.append(i);
}
return sb.toString();
}
private InputCallBack inputCallBack;//輸入完成的回調(diào)
public interface InputCallBack {
void onInputFinish(String result);
}
public void setInputCallBack(InputCallBack inputCallBack) {
this.inputCallBack = inputCallBack;
}
@Override
public boolean onCheckIsTextEditor() {
return true;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;//輸入類型為數(shù)字
outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
return new JInputConnection(this, false);
}
class JInputConnection extends BaseInputConnection {
public JInputConnection(View targetView, boolean fullEditor) {
super(targetView, fullEditor);
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
//這里是接受輸入法的文本的,我們只處理數(shù)字,所以什么操作都不做
return super.commitText(text, newCursorPosition);
}
@Override
public boolean sendKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
Log.e(TAG, event.getKeyCode() + "");
if (event.isShiftPressed()) {//處理*#等鍵
return false;
}
int keyCode = event.getKeyCode();
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {//只處理數(shù)字
if (result.size() < digit) {
result.add(keyCode - KeyEvent.KEYCODE_0);
if (getChildAt(result.size() - 1) instanceof SingleNumberView) {
Log.e(TAG, keyCode + ";");
((SingleNumberView) getChildAt(result.size() - 1)).setNumber(result.get(result.size() - 1) + "");
}
ensureFinishInput();
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_DEL) {
if (!result.isEmpty()) {//不為空,刪除最后一個(gè)
result.remove(result.size() - 1);
if (getChildAt(result.size()) instanceof SingleNumberView) {
((SingleNumberView) getChildAt(result.size())).setNumber("");
}
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_ENTER) {
ensureFinishInput();
return true;
}
}
return super.sendKeyEvent(event);
}
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
//軟鍵盤的刪除鍵 DEL 無法直接監(jiān)聽,自己發(fā)送del事件
if (beforeLength == 1 && afterLength == 0) {
return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
&& super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
}
return super.deleteSurroundingText(beforeLength, afterLength);
}
}
/**
* 判斷是否輸入完成,輸入完成后調(diào)用callback
*/
void ensureFinishInput() {
if (result.size() == digit) {//輸入完成
if (inputCallBack != null) {
StringBuffer sb = new StringBuffer();
for (int i : result) {
sb.append(i);
}
inputCallBack.onInputFinish(sb.toString());
}
}
}
}
不要被它的名字迷惑,其實(shí)它是個(gè)ViewGroup。它是LinearLayout的子類。為什么要用LinearLayout?因?yàn)槲覀兩厦嬗蟹纸膺^原型。我們需要一個(gè)水平排列View的ViewGroup。所以用LinearLayout最好不過了。因?yàn)槲覀儾粌H要布局,還要支持鍵盤輸入和自定義各種屬性,所以,我們不能直接使用LinearLayout,要自定義一個(gè)LinearLayout的子類。
構(gòu)造函數(shù)
同樣,我們先分析構(gòu)造函數(shù)。在重寫的兩個(gè)構(gòu)造函數(shù)中,都調(diào)用了init函數(shù)。我們分析下這個(gè)函數(shù):
private void init(Context context, @Nullable AttributeSet attrs) {
this.setFocusable(true);
this.setFocusableInTouchMode(true);
clearFocus();
input = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
result = new ArrayList<>();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumberInputView);
int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.NumberInputView_activeColor:
mActiveColor = a.getColor(attr, mActiveColor);
break;
case R.styleable.NumberInputView_inactiveColor:
mInactiveColor = a.getColor(attr, mInactiveColor);
break;
case R.styleable.NumberInputView_numberColor:
mTextColor = a.getColor(attr, mTextColor);
break;
case R.styleable.NumberInputView_numberTextSize:
mTextSize = a.getDimensionPixelSize(attr, mTextSize);
break;
case R.styleable.NumberInputView_spacing:
mSpacing = a.getDimensionPixelSize(attr, mSpacing);
break;
case R.styleable.NumberInputView_bottomLineWidth:
mBottomLineWidth = a.getDimensionPixelSize(attr, mBottomLineWidth);
break;
case R.styleable.NumberInputView_digit:
digit = a.getInt(attr, digit);
break;
}
}
}
首先,我們先設(shè)置支持鍵盤輸入:設(shè)置可以聚焦聚焦和獲取了輸入法管理器。然后就是支持個(gè)性化了。分析需求,我們可以知道,有這些需要個(gè)性化:底部激活線的顏色、底部基線的顏色、數(shù)字的顏色、文字的尺寸大小、文字之間的間隔、底線的寬度和接收輸入的數(shù)字的位數(shù)(四位或六位短信驗(yàn)證碼,或者更多位數(shù))。因?yàn)?,我們直接借用了LinearLayout的布局原理,所以,就沒有重寫onMeasure和onLayout方法。這里就不分析了。接下來我們看看如何實(shí)現(xiàn)支持個(gè)性化和鍵盤輸入。
支持個(gè)性化
首先,我們根據(jù)需求,定義了一個(gè)xml文檔。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="NumberInputView">
<attr name="activeColor" format="color" />
<attr name="inactiveColor" format="color" />
<attr name="numberColor" format="color" />
<attr name="numberTextSize" format="dimension" />
<attr name="spacing" format="dimension" />
<attr name="bottomLineWidth" format="dimension" />
<attr name="digit" format="integer" />
</declare-styleable>
</resources>
這樣,我們就可以在layout文件中個(gè)性化定義各種特性。
<com.jaesoon.messageverifydemo.widget.NumberInputView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/numberInputView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="25dp"
android:orientation="horizontal"
android:padding="0dp"
app:activeColor="@color/red"
/>
這樣,在我們的init方法中,就可以獲取到activeColor。
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumberInputView);
int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.NumberInputView_activeColor:
mActiveColor = a.getColor(attr, mActiveColor);
break;
case R.styleable.NumberInputView_inactiveColor:
mInactiveColor = a.getColor(attr, mInactiveColor);
break;
case R.styleable.NumberInputView_numberColor:
mTextColor = a.getColor(attr, mTextColor);
break;
case R.styleable.NumberInputView_numberTextSize:
mTextSize = a.getDimensionPixelSize(attr, mTextSize);
break;
case R.styleable.NumberInputView_spacing:
mSpacing = a.getDimensionPixelSize(attr, mSpacing);
break;
case R.styleable.NumberInputView_bottomLineWidth:
mBottomLineWidth = a.getDimensionPixelSize(attr, mBottomLineWidth);
break;
case R.styleable.NumberInputView_digit:
digit = a.getInt(attr, digit);
break;
}
}
支持鍵盤輸入
這一部分,稍微有點(diǎn)麻煩。先看代碼。
@Override
public boolean onCheckIsTextEditor() {
return true;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;//輸入類型為數(shù)字
outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
return new JInputConnection(this, false);
}
class JInputConnection extends BaseInputConnection {
public JInputConnection(View targetView, boolean fullEditor) {
super(targetView, fullEditor);
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
//這里是接受輸入法的文本的,我們只處理數(shù)字,所以什么操作都不做
return super.commitText(text, newCursorPosition);
}
@Override
public boolean sendKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
Log.e(TAG, event.getKeyCode() + "");
if (event.isShiftPressed()) {//處理*#等鍵
return false;
}
int keyCode = event.getKeyCode();
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {//只處理數(shù)字
if (result.size() < digit) {
result.add(keyCode - KeyEvent.KEYCODE_0);
if (getChildAt(result.size() - 1) instanceof SingleNumberView) {
Log.e(TAG, keyCode + ";");
((SingleNumberView) getChildAt(result.size() - 1)).setNumber(result.get(result.size() - 1) + "");
}
ensureFinishInput();
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_DEL) {
if (!result.isEmpty()) {//不為空,刪除最后一個(gè)
result.remove(result.size() - 1);
if (getChildAt(result.size()) instanceof SingleNumberView) {
((SingleNumberView) getChildAt(result.size())).setNumber("");
}
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_ENTER) {
ensureFinishInput();
return true;
}
}
return super.sendKeyEvent(event);
}
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
//軟鍵盤的刪除鍵 DEL 無法直接監(jiān)聽,自己發(fā)送del事件
if (beforeLength == 1 && afterLength == 0) {
return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
&& super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
}
return super.deleteSurroundingText(beforeLength, afterLength);
}
}
/**
* 判斷是否輸入完成,輸入完成后調(diào)用callback
*/
void ensureFinishInput() {
if (result.size() == digit) {//輸入完成
if (inputCallBack != null) {
StringBuffer sb = new StringBuffer();
for (int i : result) {
sb.append(i);
}
inputCallBack.onInputFinish(sb.toString());
}
}
}
重點(diǎn)是,我們要重寫 onCheckIsTextEditor 和 onCreateInputConnection。在 onCreateInputConnection 方法中,我們設(shè)置了彈出的鍵盤類型為數(shù)字,然后返回一個(gè)InputConnection對象。這個(gè)對象處理各種鍵盤輸入事件。 在sendKeyEvent方法中,我們根據(jù)傳入的按鍵事件,選擇自己需要的鍵值,然后進(jìn)行處理。
子View管理
當(dāng)ViewGroup出現(xiàn)在Window上時(shí),我們根據(jù)設(shè)置的數(shù)字位數(shù),動態(tài)添加SingleNumberView到布局中。
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (getChildCount() <= 0) {
for (int i = 0; i < 6; i++) {
SingleNumberView singleNumberView = new SingleNumberView(getContext(), null);
singleNumberView.setPadding(mSpacing / 2, mSpacing / 2, mSpacing / 2, mSpacing);
singleNumberView.setTextColor(mTextColor);
singleNumberView.setTextSize(mTextSize);
singleNumberView.setActiveColor(mActiveColor);
singleNumberView.setInactiveColor(mInactiveColor);
singleNumberView.setBottomLineWidth(mBottomLineWidth);
LinearLayout.LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
singleNumberView.setLayoutParams(layoutParams);
addView(singleNumberView);
}
}
}
鍵盤的管理
一個(gè)好的View需要管理好鍵盤。當(dāng)被點(diǎn)擊的時(shí)候,如果鍵盤沒有顯示,要喚出鍵盤。當(dāng)失去焦點(diǎn)時(shí),要主動的關(guān)閉鍵盤。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {//點(diǎn)擊控件彈出輸入鍵盤
requestFocus();
input.showSoftInput(this, InputMethodManager.SHOW_FORCED);
return true;
}
return super.onTouchEvent(event);
}
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
if (gainFocus) {
input.showSoftInput(this, InputMethodManager.SHOW_FORCED);
} else {
input.hideSoftInputFromInputMethod(this.getWindowToken(), 0);
}
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hasWindowFocus) {
input.hideSoftInputFromWindow(this.getWindowToken(), 0);
}
}
總結(jié)
怎么樣,一個(gè)自定義的View很簡單吧。
所以,一切的一切就是套路,學(xué)會了套路,切換到哪一端編程都游刃有余。
對了,你要的全部代碼。
嘿嘿,在這里不要臉的請大家給我一個(gè)Star。當(dāng)然,還有你的?