自定义数字输入View

有一个场景,需要输入短信验证码。So,尝试着自己设计了一个这样的View。参考了一些App,发现建设银行手机银行的短信验证码界面是我想要的。所以,设计了如下图这样两个短信输入框原型。


自定义数字输入View_第1张图片
害羞

本页图稍微有点大,可能要加载一会儿。

两种短信验证码原型图

再看一个最终的效果图。


效果图

特点

随输入的字符产生动画效果(如上图)
额,当然,图有点糊了,看的不是很清楚。
分两个场景,输入和删除

输入

当用户输入一个数字的时大概有两个效果:

  1. 文字alpha由全透明变成不透明
  2. 指示底线从中间向两边发生颜色渐变

删除

当用户删除一个数字的时大概有两个效果:

  1. 文字alpha由不透明变成透明(消失)
  2. 指示底线从两边向中间发生颜色渐变

如何实现?

写代码重要的是分解。所以,看上面的原型,我们可以这样分解:一个ViewGroup承载着几个View。ViewGroup水平布局着这些VIew。每一个View在显示和消失时,会有一个动画。如果这样分解的话,我们就很清楚了如何来实现这个效果了。
Show you the code. 代码由一个ViewGroup和一个View构成。
①. 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;

/**
 * 功能说明:
*
    *
  • 当用户输入文字之后,产生两个动画: *
      *
    1. 文字透明度变化:文字透明度由透明度100%到0%
    2. *
    3. 底部标线颜色变化:底部标线激活颜色由水平中心扩展到两端
    4. *
    *
  • *

    *

  • 当用户清除了文字之后,产生两个动画: *
      *
    1. 文字透明度变化:文字透明度由透明度0%到100%
    2. *
    3. 底部标线颜色变化:底部标线激活颜色由两端收缩到中心,然后不可见
    4. *
    *
  • *
*/ public class SingleNumberView extends View { private static final String TAG = SingleNumberView.class.getSimpleName(); /** * 相关动画:文字颜色动画、底部标线动画 */ private Animation lineExpenseAnimation; private Animation lineShrinkAnimation; /** * 动画周期 单位:ms */ private int mDuration = 500; /** * 动画百分比(不是动画消逝时间百分比) InterpolatorFraction */ private float mInterpolatorFraction = 0; /** * 当前数字 */ private String mNumber = ""; /** * 文本颜色 */ private int textColor = Color.BLACK; /** * 文本字体大小 */ private int textSize = (int) (Resources.getSystem().getDisplayMetrics().density * 25); /** * 文本为空底部文字颜色 */ private int mBottomLineEmptyColor = Color.parseColor("#47b4db"); /** * 文本为激活状态文字颜色 */ private int mBottomLineActiveColor = Color.parseColor("#6ae1ff"); /** * 底部线的宽窄 */ private int mBottomLineWidth = (int) (Resources.getSystem().getDisplayMetrics().density * 1.5); /** * 文本画笔 */ private Paint mTextPaint; /** * 标线画笔 */ 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)) { //绘制文字 //仔细推导一下,就会找到合适的居中工具(可参考引文书写四线三格) int baseline = getTextBaseline(getPaddingTop()); canvas.drawText(mNumber, getPaddingLeft() + mTextPaint.measureText("8") / 4, baseline, mTextPaint); } else { //不需要绘制文字 } //开始绘制底部基础线框 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(); //这个是文字与下划线的间隔 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); } } }

在这里插一句,一般我分析一个自定义View,首先会看构造函数,然后是onMeasure方法,在来onLayout方法,最后是onDraw方法。如果这个自定义View还定义了复杂的手势交互,可能还需要看onTouchEvent。如果是ViewGroup可能还需要看看onInterceptTouchEvent。当然,也需要看看这个View是否支持嵌套滑动。以上就是套路。

构造函数

按照上面的套路,我们首先看看构造函数。总共重写了两个构造函数。关于这两个构造函数分别是在什么时间调用,请自己百度,不在此搬运别人的分析了。共同点是,两个构造函数都调用了一个共同的函数 - 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);
    }

共创建了两个动画,分别完成我们在原型中设计的动效:
输入
当用户输入一个数字的时有两个动画效果:

  1. 文字alpha由全透明变成不透明
  2. 指示底线从中间向两边发生颜色渐变

删除
当用户删除一个数字的时有两个动画效果:

  1. 文字alpha由不透明变成透明(消失)
  2. 指示底线从两边向中间发生颜色渐变

两个动画的 applyTransformation 方法中,根据动画消逝的时间比例,计算出mInterpolatorFraction。mInterpolatorFraction是完成动画的关键因数,所有的动画效果它有关系。如,在这个方法中,紧接着就根据这个因数,设置了文字画笔mTextPaint的alpha。
此外,在init方法中,还创建了两个画笔,分别绘制数字和底部划线。

onMeasure方法

这个方法的作用是在系统绘制你的自定义View之前,先测量View的大小。如何理解?就像是我们在给墙壁贴壁纸时,首先要知道墙壁以及每一张壁纸的尺寸。我们就相当于是Android系统,墙壁就是我们的View所在的ViewGroup,View当然就相当于壁纸。在给墙壁贴壁纸之前,首先会测量墙壁和壁纸的尺寸(measure),然后布局(layout),最后贴图(draw)。同样绘制前,ViewGroup会调用我们的View的measure方法,让自定义View测量自己。你可能会说:啥?我读书少,你可不要骗我,我分明没有看到那你重写这个方法。对,你的思维很活跃。但是深度不够。如果你足够仔细的话,可以看到我们的自定义View是继承于android.view.View的。你再阅读以下View的源码,会发现,measure方法是final的,我们是无法继承的。但是,看不到,并不代表没有。在measure方法中,调用了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();
                //这个是文字与下划线的间隔
                result += getPaddingBottom();
                break;
            case MeasureSpec.EXACTLY:
                // match_parent或具体的值如:60dp
                result = heightSize;
                break;
        }
        return result;
    }

onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法有两个入参,分别是宽和高。每一个MeasureSpec方法都包含两个信息,模式和尺寸。我们可以通过MeasureSpec.getMode和MeasureSpec.getSize两个方法获取。
有三种模式,分别是:UNSPECIFIED、EXACTLY和AT_MOST:

  • UNSPECIFIED:说明父ViewGroup没有对子View强加任何限制,子View可以是它想要的任何尺寸。用得比较少,表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST,换言之,表示子布局想要多大就多大。一般出现在可以滑动的ViewGroup,很好理解,屏幕不可能无限大,既然又能支持子View想要多少就能得到多少,当然是通过滑动来实现的。如AadapterView的item的heightMode中、ScrollView的childView的heightMode中

  • EXACTLY:父ViewGroup为子View决定了一个确切的尺寸,子View将会被强制赋予这些边界限制,不管子View自己想要多大(View类onMeasure方法中只支持EXACTLY),换言之,表示设置了精确的值,一般当childView在xml或代码中设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY,即在布局文件代码中可以解析指定的具体尺寸和match_parent。

  • AT_MOST:子View可以是自己指定的任意大小,但是有个上限。比如说当MeasureSpec.EXACTLY的父容器为子级决定了一个大小,子级大小只能在这个父容器限制的范围之内。即在布局文件中可以解析wrap_content,换言之,表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST。

可以看到,在 measureWidth 方法中,我们首先判断了模式,然后根据不同的模式,给出自己的宽度值。如果是EXACTLY模式,我们就按照给定的值,给出自己的宽度。如果是UNSPECIFIED或AT_MOST模式,就设置一个数字“8”的宽度1.5倍加上左右的padding。关于高度的测量,我就不解释了,逻辑类似。

onLayout

作为一个View,就没有必要重写这方法了。

onDraw

这个是视图显示的核心部分了。

    /**
     * 开始绘制
     */
    public void onDraw(Canvas canvas) {
        //开始绘制文字
        if (!TextUtils.isEmpty(mNumber)) {
            //绘制文字
            //仔细推导一下,就会找到合适的居中工具(可参考引文书写四线三格)
            int baseline = getTextBaseline(getPaddingTop());
            canvas.drawText(mNumber, getPaddingLeft() + mTextPaint.measureText("8") / 4, baseline, mTextPaint);
        } else {
            //不需要绘制文字
        }
        //开始绘制底部基础线框
        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,浓缩才能成为精华。
这个方法,其实就做了两件事:

  • 绘制文字
  • 绘制底部标线
    • 基础标线
    • 激活标线

使用了canvas一些常见的方法,很简单。没有用过的同学,可以查看API Reference。
看到这个方法,你是否还在困惑动画是如何实现的呢?请注意一下,我们刚刚在将构造函数时,提到了init方法中的 mInterpolatorFraction 变量。这个变量一直被动画改变,在这个变量被改变之后,invalidate 方法接着被调用,地球人都知道的是:这个方法会导致View重新绘制。这意味着onDraw方法接着会被调用。而我们在绘制底部激活线时,又是根据 mInterpolatorFraction 来控制线的长短。就这样,产生了动画。简单不简单,可爱不可爱。

我不管,我最可爱

②. 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 result;//输入结果保存
    private int digit = 6;//密码位数
    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) {//点击控件弹出输入键盘
            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;//输入完成的回调

    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;//输入类型为数字
        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) {
            //这里是接受输入法的文本的,我们只处理数字,所以什么操作都不做
            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) {//只处理数字
                    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()) {//不为空,删除最后一个
                        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 无法直接监听,自己发送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);
        }
    }

    /**
     * 判断是否输入完成,输入完成后调用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());
            }
        }
    }
}

不要被它的名字迷惑,其实它是个ViewGroup。它是LinearLayout的子类。为什么要用LinearLayout?因为我们上面有分解过原型。我们需要一个水平排列View的ViewGroup。所以用LinearLayout最好不过了。因为我们不仅要布局,还要支持键盘输入和自定义各种属性,所以,我们不能直接使用LinearLayout,要自定义一个LinearLayout的子类。

构造函数

同样,我们先分析构造函数。在重写的两个构造函数中,都调用了init函数。我们分析下这个函数:

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;
            }
        }
    }

首先,我们先设置支持键盘输入:设置可以聚焦聚焦和获取了输入法管理器。然后就是支持个性化了。分析需求,我们可以知道,有这些需要个性化:底部激活线的颜色、底部基线的颜色、数字的颜色、文字的尺寸大小、文字之间的间隔、底线的宽度和接收输入的数字的位数(四位或六位短信验证码,或者更多位数)。因为,我们直接借用了LinearLayout的布局原理,所以,就没有重写onMeasure和onLayout方法。这里就不分析了。接下来我们看看如何实现支持个性化和键盘输入。

支持个性化

首先,我们根据需求,定义了一个xml文档。



    
        
        
        
        
        
        
        
    

这样,我们就可以在layout文件中个性化定义各种特性。

   

这样,在我们的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;
            }
        }

支持键盘输入

这一部分,稍微有点麻烦。先看代码。

    @Override
    public boolean onCheckIsTextEditor() {
        return true;
    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;//输入类型为数字
        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) {
            //这里是接受输入法的文本的,我们只处理数字,所以什么操作都不做
            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) {//只处理数字
                    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()) {//不为空,删除最后一个
                        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 无法直接监听,自己发送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);
        }
    }

    /**
     * 判断是否输入完成,输入完成后调用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());
            }
        }
    }

重点是,我们要重写 onCheckIsTextEditoronCreateInputConnection。在 onCreateInputConnection 方法中,我们设置了弹出的键盘类型为数字,然后返回一个InputConnection对象。这个对象处理各种键盘输入事件。 在sendKeyEvent方法中,我们根据传入的按键事件,选择自己需要的键值,然后进行处理。

子View管理

当ViewGroup出现在Window上时,我们根据设置的数字位数,动态添加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);
            }
        }
    }

键盘的管理

一个好的View需要管理好键盘。当被点击的时候,如果键盘没有显示,要唤出键盘。当失去焦点时,要主动的关闭键盘。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {//点击控件弹出输入键盘
            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);
        }
    }

总结

怎么样,一个自定义的View很简单吧。
所以,一切的一切就是套路,学会了套路,切换到哪一端编程都游刃有余。
对了,你要的全部代码。
嘿嘿,在这里不要脸的请大家给我一个Star。当然,还有你的❤

你可能感兴趣的:(自定义数字输入View)