美文网首页Android自定义控件AndroidAndroid知识
教你打造好用KeyBoard(附代码库)

教你打造好用KeyBoard(附代码库)

作者: af83084249b7 | 来源:发表于2018-05-09 21:53 被阅读701次

    起因

    各位小伙伴,开发过程中基本都要用到类似支付宝、微信那样自定义支付键盘和自定义输入框。也许,大家能找到一些差不多的类库,但是,自己搞懂逻辑,根据业务更改样式,岂不更爽?

    介绍

    关于这部分,网上有不少的实现方式。我之前有看过几个,有点耦合,不是太喜欢。所以,自己写了个,保证自己这个是完全解耦的,大家看懂即可拿着代码随意定制啦。

    效果

    keyboard.gif

    定制化

    上面是默认样式效果,具体的输入框扩展性可是很强的哦。
    具体的attrs.xml代码如下

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
     //border 指定是带方框的样式
    <declare-styleable name="border">
        //border指的是边框
        <attr name="border_color" format="color"/>
         //item指定是方框里面区域(不包含圆)
        <attr name="item_color" format="color"/>
        //interval指的是间隔线
        <attr name="interval_color" format="color"/>
        //circle指最里面的圆
        <attr name="circle_color" format="color"/>
        <attr name="border_width" format="dimension"/>
        <attr name="border_angle" format="dimension"/>
        <attr name="interval_width" format="dimension"/>
        <attr name="circle_radius" format="dimension"/>
        //num指输入个数
        <attr name="item_num" format="integer"/>
    </declare-styleable>
    
    //circle指定是实心圆样式
    <declare-styleable name="circle">
        //分为填写、未填写颜色
        <attr name="circle_selector_color" format="color"/>
        <attr name="circle_un_selector_color" format="color"/>
        <attr name="circle_circle_radius" format="dimension"/>
         //num指输入个数
        <attr name="circle_item_num" format="integer"/>
    </declare-styleable>
    </resources>
    

    思路

    整体写成一个view自然不太合适,为了更大程度的思路清晰和易于修改。输入框、数字键盘分成了两个view,然后内部处理相关逻辑,对外暴漏接口方法。最后在相关的activity或者fragment完成相关逻辑的整合处理。

    输入框:

    输入框相对来说,view绘制要求高点。我这边考虑自定义view绘制的实现方案。

    数字键盘:

    这个既可以通过recyclerview、gridview实现。但是相对臃肿一点。我们可以把相关布局放到xml里面。之后填充到我们的自定义view里面,在自定义view里面处理相关逻辑。最后,暴露接口方法给使用者。

    输入框实现

    输入框分成了两类:边框类型和圆心类型

    边框类型输入框实现

    一:定义BorderEditText 类并继承editext,设置初始化状态。
    二: 创建了四个画笔。分别是:边框、item(矩形框)、实心圆、分割线、item个数。
    三:获取attr值,如果没有采用默认值。有颜色、边框宽度、半径等
    四:用不同画笔分别绘制边框、item、实心圆、分割线。
    五:监听onTextChanged()方法,获取当前字符串长度(长度决定填充圆心个数),调用invalidate()重新绘制,展示最新的圆心数。
    六:定义接口,当字符串长度为设置的item个数时,回调方法。在回调方法处理逻辑。

     public class BorderEditText extends android.support.v7.widget.AppCompatEditText {
    private Context mContext;
    private Paint mBorderPaint;
    private Paint mIntervalPaint;
    private Paint mCirclePaint;
    private int mNum;
    private int mLength;
    private float mCircleRadius;
    private float mBorderAngle;
    private Paint mItemPaint;
    private float mBorderWidth;
    
    public BorderEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        initPaints();
        initAttrs(attrs);
        setFilters(new InputFilter[]{new InputFilter.LengthFilter(mNum)});
        setInputType(InputType.TYPE_CLASS_NUMBER);
        setBackgroundDrawable(null);
        setFocusable(false);
    }
    
    private void initPaints() {
        mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mItemPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mIntervalPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }
    
    private void initAttrs(AttributeSet attrs) {
        TypedArray typedArray = mContext.obtainStyledAttributes(attrs,
                R.styleable.border, 0, 0);
        //外边框相关
        int borderColor = typedArray.getColor(R.styleable.border_border_color, mContext.getResources().getColor(R.color.border_color));
        mBorderAngle = DensityUtil.dp2px(mContext, typedArray.getDimension(R.styleable.border_border_angle, 10));
        mBorderWidth = DensityUtil.dp2px(mContext, typedArray.getDimension(R.styleable.border_border_width, 1));
        //item颜色
        int itemColor = typedArray.getColor(R.styleable.border_border_color, mContext.getResources().getColor(R.color.withe));
        //间隔线相关
        int intervalColor = typedArray.getColor(R.styleable.border_interval_color, mContext.getResources().getColor(R.color.interval_color));
        float intervalWidth = DensityUtil.dp2px(mContext, typedArray.getDimension(R.styleable.border_interval_width, 1));
        //实心圆相关
        int circleColor = typedArray.getColor(R.styleable.border_circle_color, mContext.getResources().getColor(R.color.circle_color));
        mCircleRadius = DensityUtil.dp2px(mContext, typedArray.getDimension(R.styleable.border_circle_radius, 5));
        //num个数
        mNum = typedArray.getInteger(R.styleable.border_item_num, 6);
    
    
        mBorderPaint.setColor(borderColor);
        mBorderPaint.setStyle(Paint.Style.FILL);
    
        mItemPaint.setColor(itemColor);
        mItemPaint.setStyle(Paint.Style.FILL);
    
        mIntervalPaint.setColor(intervalColor);
        mIntervalPaint.setStyle(Paint.Style.STROKE);
        mIntervalPaint.setStrokeWidth(intervalWidth);
    
        mCirclePaint.setColor(circleColor);
        mCirclePaint.setStyle(Paint.Style.FILL);
    
        typedArray.recycle();
    
    }
    
    
    @Override
    protected void onDraw(Canvas canvas) {
        //画边框
        int width = getWidth();
        int height = getHeight();
        RectF borderRectF = new RectF(0, 0, width, height);
        canvas.drawRoundRect(borderRectF, mBorderAngle, mBorderAngle, mBorderPaint);
        //画item
        RectF itemRectF = new RectF(mBorderWidth, mBorderWidth, width - mBorderWidth, height - mBorderWidth);
        canvas.drawRoundRect(itemRectF, mBorderAngle, mBorderAngle, mItemPaint);
        //画间隔线
        int itemWidth = getWidth() / mNum;
        for (int i = 1; i < mNum; i++) {
            int offsetX = itemWidth * i;
            canvas.drawLine(offsetX, 0, offsetX, height, mIntervalPaint);
        }
        //画实心圆
        for (int i = 0; i < mLength; i++) {
            float circleX = (float) (itemWidth * i + itemWidth * 0.5);
            float circleY = (float) (getHeight() * 0.5);
            canvas.drawCircle(circleX, circleY, mCircleRadius, mCirclePaint);
        }
    
    }
    
    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        mLength = text.toString().length();
        invalidate();
        if (mListener != null) {
            if (mLength == mNum) {
                mListener.OnBorderEditTextComplete(this, text.toString());
            }
        }
    }
    
    public void clear() {
        setText("");
    }
    
    public interface OnBorderEditTextListener {
        void OnBorderEditTextComplete(BorderEditText editText, String text);
    }
    
    public OnBorderEditTextListener mListener;
    
    public void setListener(OnBorderEditTextListener listener) {
        mListener = listener;
    }
    }
    
    实心圆类型输入框实现

    一:定义CircleEditText 类并继承editext,设置初始化状态。
    二: 创建了二个画笔。分别是:圆心填充,圆心未填画笔。
    三:获取attr值,如果没有定义则采用默认值。有颜色、边框宽度、半径等。
    四:根据状态,用不同画笔,绘制不同位置的实心圆。
    五:监听onTextChanged()方法,获取当前字符串长度(长度决定填充圆心个数),调用invalidate()重新绘制,更改状态。
    六:定义接口,当字符串长度为设置的item个数时,回调方法。在回调方法处理逻辑。

    public class CircleEditText extends android.support.v7.widget.AppCompatEditText {
    
    private Context mContext;
    private Paint mSelectorPaint;
    private Paint mUnSelectorPaint;
    private int mNum;
    private int mLength;
    private float mCircleRadius;
    
    public CircleEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        initPaints();
        initAttrs(attrs);
        setFilters(new InputFilter[]{new InputFilter.LengthFilter(mNum)});
        setInputType(InputType.TYPE_CLASS_NUMBER);
        setBackgroundDrawable(null);
        setFocusable(false);
    
    }
    
    private void initPaints() {
        mSelectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mUnSelectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }
    
    private void initAttrs(AttributeSet attrs) {
        TypedArray typedArray = mContext.obtainStyledAttributes(attrs,
                R.styleable.circle, 0, 0);
        int selectorColor = typedArray.getColor(R.styleable.circle_circle_selector_color, mContext.getResources().getColor(R.color.selector_color));
        int unSelectorColor = typedArray.getColor(R.styleable.circle_circle_un_selector_color, mContext.getResources().getColor(R.color.un_selector_color));
    
        mCircleRadius = DensityUtil.dp2px(mContext, typedArray.getDimension(R.styleable.circle_circle_circle_radius, 10));
    
        mNum = typedArray.getInteger(R.styleable.circle_circle_item_num, 6);
    
        mSelectorPaint.setColor(selectorColor);
        mSelectorPaint.setStyle(Paint.Style.FILL);
    
        mUnSelectorPaint.setColor(unSelectorColor);
        mUnSelectorPaint.setStyle(Paint.Style.FILL);
    
        typedArray.recycle();
    
    
    }
    
    
    @Override
    protected void onDraw(Canvas canvas) {
        int itemWidth = getWidth() / mNum;
        for (int i = 0; i < mNum; i++) {
            float circleX = (float) (itemWidth * i + itemWidth * 0.5);
            float circleY = (float) (getHeight() * 0.5);
            if (i < mLength) {
                canvas.drawCircle(circleX, circleY, mCircleRadius, mSelectorPaint);
            } else {
                canvas.drawCircle(circleX, circleY, mCircleRadius, mUnSelectorPaint);
            }
        }
    
    }
    
    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        mLength = text.toString().length();
        invalidate();
        if (mListener != null) {
            if (mLength == mNum) {
                mListener.OnCircleEditTextComplete(this, text.toString());
            }
        }
    }
    
    public void clear() {
        setText("");
    }
    
    public interface OnCircleEditTextListener {
        void OnCircleEditTextComplete(CircleEditText editText, String text);
    }
    
    public OnCircleEditTextListener mListener;
    
    public void setListener(OnCircleEditTextListener listener) {
        mListener = listener;
    }
    }
    

    数字键盘

    一:自定义NumberInputView ,在里面完成必要逻辑,对外暴漏接口。
    一:填充布局,布局在xml中主要通过TableLayout实现排版。
    三:获取xml里面的具体控件,并设置监听。为了简化不必要代码,我通过view数组方式,设置监听。
    四:定义接口方法。根据不同按钮,回调不同的方法。分了三类:数字按钮、清空按钮、回退按钮。
    public class NumberInputView extends LinearLayout {

    private Context mContext;
    private View[] mViews;
    
    public NumberInputView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        initViews();
        initListeners();
    
    }
    
    
    private void initViews() {
        View inflate = LayoutInflater.from(mContext).inflate(R.layout.number_input_view, this, true);
        View zero = inflate.findViewById(R.id.zero);
        View one = inflate.findViewById(R.id.one);
        View two = inflate.findViewById(R.id.two);
        View three = inflate.findViewById(R.id.three);
        View four = inflate.findViewById(R.id.four);
        View five = inflate.findViewById(R.id.five);
        View six = inflate.findViewById(R.id.six);
        View seven = inflate.findViewById(R.id.seven);
        View eight = inflate.findViewById(R.id.eight);
        View nine = inflate.findViewById(R.id.nine);
        View clear = inflate.findViewById(R.id.clear);
        View delete = inflate.findViewById(R.id.backward);
        mViews = new View[]{zero, one, two, three, four, five, six, seven, eight, nine, clear, delete};
    }
    
    private void initListeners() {
        for (int i = 0; i < mViews.length; i++) {
            final int finalI = i;
            mViews[i].setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mListener == null) {
                        return;
                    }
                    if (finalI == 10) {
                        mListener.onClearClick(NumberInputView.this);
                        return;
                    }
                    if (finalI == 11) {
                        mListener.onBackwardClick(NumberInputView.this);
                        return;
                    }
                    mListener.onNumberClick(NumberInputView.this, finalI);
    
                }
            });
        }
    }
    
    public interface OnNumberInputViewListener {
        void onNumberClick(NumberInputView view, int num);
    
        void onClearClick(NumberInputView view);
    
        void onBackwardClick(NumberInputView view);
    }
    
    public OnNumberInputViewListener mListener;
    
    public void setListener(OnNumberInputViewListener listener) {
        mListener = listener;
    }
    

    }

    回调的使用

       @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_border);
        mBorderEditText = (BorderEditText) findViewById(R.id.edit);
        mBorderEditText.setListener(new BorderEditText.OnBorderEditTextListener() {
            @Override
            public void OnBorderEditTextComplete(BorderEditText editText, String text) {
             //输入123456模拟验证成功,其他模拟失败。
                if (text.equals("123456")) {
                    Toast.makeText(BorderActivity.this, " success  " + text, Toast.LENGTH_SHORT).show();
                    return;
                }
                Toast.makeText(BorderActivity.this," error  "+ text, Toast.LENGTH_SHORT).show();
                //开启错误动画  与下面clear(clear方式无动画) 二选一
                errorAnim();
              // mBorderEditText.clear();
    
            }
        });
        NumberInputView numberInputView = (NumberInputView) findViewById(R.id.input);
        numberInputView.setListener(new NumberInputView.OnNumberInputViewListener() {
            @Override
            //数字键回调
            public void onNumberClick(NumberInputView view, int num) {
                //mBorderEditText 字符串长度加1
                if (!mBorderEditText.isEnabled()) {   //错误样式动画执行期间,设置不可输入。
                    return;
                }
                String s = mBorderEditText.getText().toString();
                s += num;
                //自定义边框输入框,设置text(会触发里面的ontextChanged方法,从而执行重绘)
                mBorderEditText.setText(s);
            }
               
            @Override
            //清空按钮
            public void onClearClick(NumberInputView view) {
                //清空输入框内容
                mBorderEditText.clear();
            }
    
            @Override
            //回退一个按钮
            public void onBackwardClick(NumberInputView view) {
                //mBorderEditText 字符串长度减1
                String s = mBorderEditText.getText().toString();
                if (s.length() == 0) {
                    return;
                }
                String substring = s.substring(0, s.length() - 1);
                 //自定义边框输入框,设置text(会触发里面的ontextChanged方法,从而执行重绘)
                mBorderEditText.setText(substring);
            }
        });
    }
    

    通过以上方法,输入框、数字键盘完全无耦合。方便大家书写自己的逻辑。

    总结

    无耦合,易修改的一套Keyboard,希望能帮助到大家。后期会在此基础上,开源完全解耦的应用锁(pin码)。尽请期待~

    地址:https://github.com/HoldMyOwn/Keyboard.git

    相关文章

      网友评论

      本文标题:教你打造好用KeyBoard(附代码库)

      本文链接:https://www.haomeiwen.com/subject/jgqbrftx.html