自定義數(shù)字輸入View

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


害羞

本頁圖稍微有點(diǎn)大,可能要加載一會兒。

兩種短信驗(yàn)證碼原型圖

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


效果圖

特點(diǎn)

隨輸入的字符產(chǎn)生動畫效果(如上圖)
額,當(dāng)然,圖有點(diǎn)糊了,看的不是很清楚。
分兩個(gè)場景,輸入和刪除

輸入

當(dāng)用戶輸入一個(gè)數(shù)字的時(shí)大概有兩個(gè)效果:

  1. 文字alpha由全透明變成不透明
  2. 指示底線從中間向兩邊發(fā)生顏色漸變

刪除

當(dāng)用戶刪除一個(gè)數(shù)字的時(shí)大概有兩個(gè)效果:

  1. 文字alpha由不透明變成透明(消失)
  2. 指示底線從兩邊向中間發(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è)動畫效果:

  1. 文字alpha由全透明變成不透明
  2. 指示底線從中間向兩邊發(fā)生顏色漸變

刪除
當(dāng)用戶刪除一個(gè)數(shù)字的時(shí)有兩個(gè)動畫效果:

  1. 文字alpha由不透明變成透明(消失)
  2. 指示底線從兩邊向中間發(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)是,我們要重寫 onCheckIsTextEditoronCreateInputConnection。在 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)然,還有你的?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,188評論 25 708
  • 國內(nèi)自定義View的文章汗牛充棟,但是,即便是你全部看完也未必掌握這一知識(實(shí)際上,我也看了很多,但是一旦涉及自定...
    SnowDragonYY閱讀 1,721評論 3 36
  • 國內(nèi)自定義View的文章汗牛充棟,但是,即使你全部看完它們也未必能掌握這一知識點(diǎn)(實(shí)際上,我就幾乎看完了所有的國內(nèi)...
    工程師milter閱讀 37,947評論 143 673
  • 《灌籃高手》,一部比我大十歲的日本漫畫,不過我不看他的漫畫,我看動漫。雖然動漫沒有把漫畫出完,其實(shí)漫畫某種意義上也...
    5d9430d70325閱讀 287評論 1 0
  • 進(jìn)修結(jié)束,受益匪淺,它讓我在描繪百味人生的調(diào)色板上多添了一抹亮色,站在別人的地盤上好想更能發(fā)現(xiàn)自己工作中的問題...
    堅(jiān)持行動家閱讀 541評論 0 2

友情鏈接更多精彩內(nèi)容