最近项目中在做一个股票交易需求升级, 产品对于输入方式有一些特殊的要求, 具体就是对于输入键盘加了诸多限制. 这就必须需要自定义键盘来完成需求.
效果如下:
具体需求:
- 当焦点在股票价格编辑框上时, 键盘弹出时不能遮盖住卖出数量.
即键盘弹出是以两个输入框底部为基线的. - 键盘弹击要有一个向上推出的动画效果.
- 两个输入框弹出不同的键盘界面,
股票价格输入框 弹出数字键盘
股票数量输入框 弹出数量键盘(如上图)
最终的实现效果:
简书上找了些自定义键盘的例子, 基本都不能满足我的需求, 但是给了我一个很好的切入点. 在此非常感谢!
参考其实现, 我做了些封装. 做了一个自定义键盘的工具类,
设计原则:与外界充分解耦,通过自定议键盘管理者, 绑定对应输入框和键盘,键盘的实现者仅需要关注特殊按键的响应处理.
设计原理:通过传入activity获得其DecorView,添加键盘布局。将键盘布局set到屏幕底部,当输入框获得焦点时,如果设置了基线view, 则判断基线view所在位置, 否则默认以输入框为基线View,若键盘弹出会遮挡基线View,则屏幕整体向上滑动一定的距离:
屏幕移动高度为:
移动距离 = 基线View到屏幕顶部距离 + 自定义键盘高度 - 整个屏幕高度
if 移动距离 > 0 则说明当键盘加入到根布局后, 屏幕无法完成加载, 需要屏幕向上滚动一定的偏移量.
if 移动距离 <= 0 则说明键盘弹出后还没有达到基线设置位置, 不需要滚动整个屏幕.
计算屏幕需要移动的偏移量:
/**
* 计算屏幕向上移动距离
* @param view 响应输入焦点的控件
* @return 移动偏移量
*/
private int getMoveHeight(View view) {
Rect rect = new Rect();
mRootView.getWindowVisibleDisplayFrame(rect); //获取当前显示区域的宽高
int[] vLocation = new int[2];
view.getLocationOnScreen(vLocation); //计算输入框在屏幕中的位置
int keyboardTop = vLocation[1] + view.getHeight() + view.getPaddingBottom() + view.getPaddingTop();
if (keyboardTop - mKeyboardHeight < 0) { //如果输入框到屏幕顶部已经不能放下键盘的高度, 则不需要移动了.
return 0;
}
if (null != mShowUnderView) { //如果有基线View. 则计算基线View到屏幕的距离
int[] underVLocation = new int[2];
mShowUnderView.getLocationOnScreen(underVLocation);
keyboardTop = underVLocation[1] + mShowUnderView.getHeight() + mShowUnderView.getPaddingBottom() + mShowUnderView.getPaddingTop();
}
//输入框或基线View的到屏幕的距离 + 键盘高度 如果 超出了屏幕的承载范围, 就需要移动.
int moveHeight = keyboardTop + mKeyboardHeight - rect.bottom;
return moveHeight > 0 ? moveHeight : 0;
}
显示自定义的键盘:
public void showSoftKeyboard(EditText view) {
BaseKeyboard keyboard = getKeyboard(view); //获取输入框所绑定的键盘BaseKeyboard
if (null == keyboard) {
Log.e(TAG, "The EditText not bind BaseKeyboard!");
return;
}
keyboard.setCurEditText(view);
keyboard.setNextFocusView(etFocusScavenger); //为键盘设置下一个焦点响应控件.
refreshKeyboard(keyboard); //设置键盘keyboard到KeyboardView中.
//将键盘布局加入到根布局中.
mRootView.addView(mKeyboardViewContainer, mKeyboardViewLayoutParams);
//设置加载动画.
mKeyboardViewContainer.setAnimation(AnimationUtils.loadAnimation(mActivity, R.anim.down_to_up));
int moveHeight = getMoveHeight(view);
if (moveHeight > 0) {
mRootView.getChildAt(0).scrollBy(0, moveHeight); //移动屏幕
} else {
moveHeight = 0;
}
view.setTag(R.id.keyboard_view_move_height, moveHeight);
}
隐藏自定义的键盘
public void hideSoftKeyboard(EditText view) {
int moveHeight = 0;
Object tag = view.getTag(R.id.keyboard_view_move_height);
if (null != tag) moveHeight = (int) tag;
if (moveHeight > 0) { //复原屏幕
mRootView.getChildAt(0).scrollBy(0, -1 * moveHeight);
view.setTag(R.id.keyboard_view_move_height, 0);
}
mRootView.removeView(mKeyboardViewContainer); //将键盘从根布局中移除.
mKeyboardViewContainer.setAnimation(AnimationUtils.loadAnimation(mActivity, R.anim.up_to_hide));
}
为了适应不同的键盘布局, 有必要定义一个Keyboard的基类, 所有的自定义键盘都继承于它. 并且它响应KeyboardView.OnKeyboardActionListener的所有接口.
public abstract class CustomBaseKeyboard extends Keyboard implements KeyboardView.OnKeyboardActionListener{
protected EditText etCurrent;
protected View nextFocusView;
protected CustomKeyStyle customKeyStyle;
public CustomBaseKeyboard(Context context, int xmlLayoutResId) {
super(context, xmlLayoutResId);
}
public CustomBaseKeyboard(Context context, int xmlLayoutResId, int modeId, int width, int height) {
super(context, xmlLayoutResId, modeId, width, height);
}
public CustomBaseKeyboard(Context context, int xmlLayoutResId, int modeId) {
super(context, xmlLayoutResId, modeId);
}
public CustomBaseKeyboard(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding) {
super(context, layoutTemplateResId, characters, columns, horizontalPadding);
}
protected int getKeyCode(int resId) {
if (null != etCurrent) {
return etCurrent.getContext().getResources().getInteger(resId);
} else {
return Integer.MIN_VALUE;
}
}
public void setCurEditText(EditText etCurrent) {
this.etCurrent = etCurrent;
}
public EditText getCurEditText() {
return etCurrent;
}
public void setNextFocusView(View view) {
this.nextFocusView = view;
}
public CustomKeyStyle getCustomKeyStyle() {
return customKeyStyle;
}
public void setCustomKeyStyle(CustomKeyStyle customKeyStyle) {
this.customKeyStyle = customKeyStyle;
}
@Override
public void onPress(int primaryCode) {
}
@Override
public void onRelease(int primaryCode) {
}
@Override
public void onKey(int primaryCode, int[] keyCodes) {
if (null != etCurrent && etCurrent.hasFocus() && !handleSpecialKey(etCurrent, primaryCode)) {
Editable editable = etCurrent.getText();
int start = etCurrent.getSelectionStart();
if (primaryCode == Keyboard.KEYCODE_DELETE) { //回退
if (!TextUtils.isEmpty(editable)) {
if (start > 0) {
editable.delete(start - 1, start);
}
}
} else if (primaryCode == getKeyCode(R.integer.keycode_empty_text)) { //清空
editable.clear();
} else if (primaryCode == getKeyCode(R.integer.keycode_hide_keyboard)) { //隐藏
hideKeyboard();
} else if (primaryCode == 46) { //小数点
if (!editable.toString().contains(".")) {
editable.insert(start, Character.toString((char) primaryCode));
}
} else { //其他默认
editable.insert(start, Character.toString((char) primaryCode));
}
}
//getKeyboardView().postInvalidate();
}
public void hideKeyboard() {
//hideSoftKeyboard(etCurrent);
if (null != nextFocusView) nextFocusView.requestFocus();
}
/**
* 处理自定义键盘的特殊定制键
* 注: 所有的操作要针对etCurrent来操作
*
* @param etCurrent 当前操作的EditText
* @param primaryCode 选择的Key
* @return true: 已经处理过, false: 没有被处理
*/
public abstract boolean handleSpecialKey(EditText etCurrent, int primaryCode);
...... //其它的默认空实现
}
当自定义键盘时, 仅需要去实现handleSpecialKey接口, 处理键盘中自定义键
在BaseKeyboard中已经默认实现了基础的输入字符, 和 回退, 清空, 隐藏.
当然在构造时也必须传入Keyboard所必需的参数 context 和 键盘布局xml
如下:
customKeyboardManager = new CustomKeyboardManager(mActivity);
CustomKeyboardManager.BaseKeyboard priceKeyboard = new CustomKeyboardManager.BaseKeyboard(getContext(), R.xml.stock_price_num_keyboard) {
@Override
public boolean handleSpecialKey(EditText etCurrent, int primaryCode) {
if (primaryCode == getKeyCode( R.integer.keycode_cur_price)) {
etCurrent.setText("9.99");
return true;
}
return false;
}
};
//为etInputPrice1和etInputPrice2都定制priceKeyboard键盘.
customKeyboardManager.attachTo(etInputPrice1, priceKeyboard);
customKeyboardManager.attachTo(etInputPrice2, priceKeyboard);
customKeyboardManager.attachTo(etInputNum, new CustomKeyboardManager.BaseKeyboard(getContext(), R.xml.stock_trade_num_keyboard) {
@Override
public boolean handleSpecialKey(EditText etCurrent, int primaryCode) {
Editable editable = etCurrent.getText();
int start = etCurrent.getSelectionEnd();
if (primaryCode == getKeyCode( R.integer.keycode_stocknum_000)) {
editable.insert(start, "000");
return true;
} else if (primaryCode == getKeyCode(R.integer.keycode_stocknum_all)){ //全仓
setStockNumAll(etCurrent);
return true;
}
return false;
}
});
customKeyboardManager.setShowUnderView(underView); //设置键盘弹出所达到的基线View
另外在attachTo(editText, baseKeyboard)时, 会设置editText隐藏系统键盘. 设置其绑定的keyboard, 设置FocusChangeListener事件监听.
下面是键盘布局:
<?xml version="1.0" encoding="UTF-8"?><!-- 数字键盘 -->
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
android:horizontalGap="2dp"
android:keyHeight="62dp"
android:keyWidth="20%p"
android:verticalGap="2dp">
<Row>
<Key
android:codes="@integer/keycode_stocknum_all"
android:keyEdgeFlags="left"
android:keyLabel="全仓"/>
<Key
android:codes="49"
android:keyLabel="1" />
<Key
android:codes="50"
android:keyLabel="2" />
<Key
android:codes="51"
android:keyLabel="3" />
<Key
android:codes="-5"
android:keyLabel="回退"
android:iconPreview="@drawable/bg_custom_key_light_gray"/>
</Row>
<Row>
<Key
android:codes="@integer/keycode_stocknum_half"
android:keyEdgeFlags="left"
android:keyLabel="半仓"/>
<Key
android:codes="52"
android:keyLabel="4" />
<Key
android:codes="53"
android:keyLabel="5" />
<Key
android:codes="54"
android:keyLabel="6" />
<Key
android:codes="@integer/keycode_empty_text"
android:keyLabel="清空"
android:iconPreview="@drawable/bg_custom_key_light_gray"/>
</Row>
<Row>
<Key
android:codes="@integer/keycode_stocknum_1_3"
android:keyEdgeFlags="left"
android:keyLabel="1/3仓"/>
<Key
android:codes="55"
android:keyLabel="7" />
<Key
android:codes="56"
android:keyLabel="8" />
<Key
android:codes="57"
android:keyLabel="9" />
<Key
android:codes="@integer/keycode_hide_keyboard"
android:keyLabel="隐藏"
android:iconPreview="@drawable/bg_custom_key_light_gray"/>
</Row>
<Row>
<Key
android:codes="@integer/keycode_stocknum_1_4"
android:keyEdgeFlags="left"
android:keyLabel="1/4仓"
android:keyWidth="20%p"/>
<Key
android:codes="@integer/keycode_stocknum_000"
android:isRepeatable="true"
android:keyLabel="000"
android:keyWidth="20%p"/>
<Key
android:codes="48"
android:keyLabel="0"
android:keyWidth="20%p"/>
<Key
android:codes="@integer/keycode_stock_sell"
android:keyLabel="卖出"
android:iconPreview="@drawable/bg_custom_key_blue"
android:keyWidth="40%p"/>
</Row>
</Keyboard>
对于我们特殊定制的key的code为了唯一性的原则, 这里将其统一定义在res/values/custom_keyboard.xml中
<!--股票数量键盘-->
<integer name="keycode_stocknum_000">-10200</integer>
<integer name="keycode_stocknum_all">-10201</integer>
<integer name="keycode_stocknum_half">-10202</integer>
<integer name="keycode_stocknum_1_3">-10203</integer>
<integer name="keycode_stocknum_1_4">-10204</integer>
<integer name="keycode_stock_sell">-10205</integer>
可是至此, 仍有一个问题没法解决, 那就是对于每个Key的样式的定制. 看遍源码中, 也没有找到关于这些设置, 有的只是针对KeyboardView的设置. 但是这些设置会统一应用到所有按键上, 还是无法实现对每个按键的独立定制样式.
//源码中对xml布局中key的解析如下:
public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) {
this(parent);
...........
width = getDimensionOrFraction(a,
com.android.internal.R.styleable.Keyboard_keyWidth,
keyboard.mDisplayWidth, parent.defaultWidth);
height = getDimensionOrFraction(a,
com.android.internal.R.styleable.Keyboard_keyHeight,
keyboard.mDisplayHeight, parent.defaultHeight);
gap = getDimensionOrFraction(a,
com.android.internal.R.styleable.Keyboard_horizontalGap,
keyboard.mDisplayWidth, parent.defaultHorizontalGap);
........
难道以上都白做了么?
...
...
...
经过一番细读源码, 决定对KeyboardView进行扩展.
- 首先Keyboard描述了键盘的布局(通过给定的xml),并解析它,
CustomBaseKeyboard及其实现,扩展了其对按键的处理与EditText的联系. - KeyboardView 是承载不同的keyboard并绘制keyboard, 就像是键盘布局的绘制板, 并与系统交互.
扩展思路:
通过扩展的KeyboardView, 对其绘制过程做定制操作, 就可以实现对每个按键样式的定制了
而KeyboardView的绘制过程并没有给我们任何机会去对其扩展定制.
源码参考
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/inputmethodservice/KeyboardView.java#634
为此只能通过对KeyboardView的重新绘制才能实现.
具体就是重写onDraw方法, 在onDraw方法中通过接口调用实现定制.
并用反射的方法解决需要依赖的KeyboardView中的属性.
代码片段如下:
public class CustomKeyboardView extends KeyboardView {
private static final String TAG = "CustomKeyboardView";
private Drawable rKeyBackground;
private int rLabelTextSize;
private int rKeyTextSize;
private int rKeyTextColor;
private float rShadowRadius;
private int rShadowColor;
private Rect rClipRegion;
private Keyboard.Key rInvalidatedKey;
...........
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
rKeyBackground = (Drawable) ReflectionUtils.getFieldValue(this, "mKeyBackground");
rLabelTextSize = (int) ReflectionUtils.getFieldValue(this, "mLabelTextSize");
rKeyTextSize = (int) ReflectionUtils.getFieldValue(this, "mKeyTextSize");
rKeyTextColor = (int) ReflectionUtils.getFieldValue(this, "mKeyTextColor");
rShadowColor = (int) ReflectionUtils.getFieldValue(this, "mShadowColor");
rShadowRadius = (float) ReflectionUtils.getFieldValue(this, "mShadowRadius");
}
@Override
public void onDraw(Canvas canvas) {
//说明CustomKeyboardView只针对CustomBaseKeyboard键盘进行重绘,
// 且CustomBaseKeyboard必需有设置CustomKeyStyle的回调接口实现, 才进行重绘, 这才有意义
if(null == getKeyboard() || !(getKeyboard() instanceof CustomBaseKeyboard) || null == ((CustomBaseKeyboard)getKeyboard()).getCustomKeyStyle()){
Log.e(TAG, "");
super.onDraw(canvas);
return;
}
rClipRegion = (Rect) ReflectionUtils.getFieldValue(this, "mClipRegion");
rInvalidatedKey = (Keyboard.Key) ReflectionUtils.getFieldValue(this, "mInvalidatedKey");
super.onDraw(canvas);
onRefreshKey(canvas);
}
/**
* onRefreshKey是对父类的private void onBufferDraw()进行的重写. 只是在对key的绘制过程中进行了重新设置.
* @param canvas
*/
private void onRefreshKey(Canvas canvas) {
........
//拿到当前键盘被弹起的输入源 和 键盘为每个key的定制实现customKeyStyle
EditText etCur = ((CustomBaseKeyboard)getKeyboard()).getCurEditText();
CustomBaseKeyboard.CustomKeyStyle customKeyStyle = ((CustomBaseKeyboard)getKeyboard()).getCustomKeyStyle();
List<Keyboard.Key> keys = getKeyboard().getKeys();
final int keyCount = keys.size();
//canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
for (int i = 0; i < keyCount; i++) {
final Keyboard.Key key = keys.get(i);
//获取为Key自定义的背景, 若没有定制, 使用KeyboardView的默认属性keyBackground设置
keyBackground = customKeyStyle.getKeyBackground(key, etCur);
if(null == keyBackground){ keyBackground = rKeyBackground; }
......
//获取为Key自定义的Label, 若没有定制, 使用xml布局中指定的
CharSequence keyLabel = customKeyStyle.getKeyLabel(key, etCur);
.....
canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
keyBackground.draw(canvas);
if (label != null) {
//获取为Key的Label的字体大小, 若没有定制, 使用KeyboardView的默认属性keyTextSize设置
Float customKeyTextSize = customKeyStyle.getKeyTextSize(key, etCur);
// For characters, use large font. For labels like "Done", use small font.
if(null != customKeyTextSize){
paint.setTextSize(customKeyTextSize);
paint.setTypeface(Typeface.DEFAULT_BOLD);
} else {
....
}
//获取为Key的Label的字体颜色, 若没有定制, 使用KeyboardView的默认属性keyTextColor设置
Integer customKeyTextColor = customKeyStyle.getKeyTextColor(key, etCur);
if(null != customKeyTextColor) {
paint.setColor(customKeyTextColor);
} else {
paint.setColor(rKeyTextColor);
}
具体的定制样式接口在CustomBaseKeyboard中定义:
public interface CustomKeyStyle {
Drawable getKeyBackground(Key key, EditText etCur);
Float getKeyTextSize(Key key, EditText etCur);
Integer getKeyTextColor(Key key, EditText etCur);
CharSequence getKeyLabel(Key key, EditText etCur);
}
为了保证我们自定义的键盘都能够在使用了CustomKeyboardView时, 都能进行重绘, 在CustomKeyboardManager的attachTo中还要主动为其设置一个默认的实现.
public void attachTo(EditText editText, CustomBaseKeyboard keyboard) {
hideSystemSoftKeyboard(editText);
editText.setTag(R.id.edittext_bind_keyboard, keyboard);
if(null == keyboard.getCustomKeyStyle()) keyboard.setCustomKeyStyle(defaultCustomKeyStyle);
editText.setOnFocusChangeListener(this);
}
在使用的时候就需要加入对keyboard的样式设置
numKeyboard.setCustomKeyStyle(new CustomBaseKeyboard.SimpleCustomKeyStyle(){
@Override
public Drawable getKeyBackground(Keyboard.Key key, EditText etCur) {
if(getKeyCode(etCur.getContext(), R.integer.keycode_stock_sell) == key.codes[0]) {
if (R.id.et_input_num_sell == etCur.getId()) {
return getDrawable(etCur.getContext(), R.drawable.bg_custom_key_blue);
} else if (R.id.et_input_num_buy == etCur.getId()) {
return getDrawable(etCur.getContext(), R.drawable.bg_custom_key_red);
}
}
return super.getKeyBackground(key, etCur);
}
@Override
public CharSequence getKeyLabel(Keyboard.Key key, EditText etCur) {
if(getKeyCode(etCur.getContext(), R.integer.keycode_stock_sell) == key.codes[0]) {
if (R.id.et_input_num_sell == etCur.getId()) {
return "卖出";
} else if (R.id.et_input_num_buy == etCur.getId()) {
return "买入";
}
}
return super.getKeyLabel(key, etCur);
}
});
文中代码多有省略, 时间仓促且本人能力有限, 仅是对当前项目中的实现做的�定制, 不一定能适用所有的项目, 只是提供了一种参考实现, 相信一定有更好的解决方案, 还请留下你的思路方案, 共同进步, 如有缺陷还请留言, 共同解决成长! _
参考:
http://www.jianshu.com/p/8fb70cadca27
http://www.jianshu.com/p/aedf6f456560
http://931360439-qq-com.iteye.com/blog/938886
具体请参考我的Github
https://github.com/kangqiao182/CustomKeyboard