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

code小生,一个专注于 Android 领域的技术分享平台

作者:Allen___
地址:https://www.jianshu.com/p/d2c6e6e59335
声明:本文是 Allen___ 原创,转发等请联系原作者授权。

起因

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

介绍

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

效果

keyboard.gif

定制化

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


<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

键盘

仿微信表情输入键盘(支持 Gif 表情图文混排 )

Android 键盘适配-中英文适配

你可能感兴趣的:(教你打造好用KeyBoard(附代码库))