Android自定义控件AndroidAndroid知识

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

2018-05-09  本文已影响701人  af83084249b7

起因

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

介绍

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

效果

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

上一篇 下一篇

猜你喜欢

热点阅读