教你打造好用KeyBoard(附代码库)
起因
各位小伙伴,开发过程中基本都要用到类似支付宝、微信那样自定义支付键盘和自定义输入框。也许,大家能找到一些差不多的类库,但是,自己搞懂逻辑,根据业务更改样式,岂不更爽?
介绍
关于这部分,网上有不少的实现方式。我之前有看过几个,有点耦合,不是太喜欢。所以,自己写了个,保证自己这个是完全解耦的,大家看懂即可拿着代码随意定制啦。
效果
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码)。尽请期待~