最近項(xiàng)目中在做一個股票交易需求升級, 產(chǎn)品對于輸入方式有一些特殊的要求, 具體就是對于輸入鍵盤加了諸多限制. 這就必須需要自定義鍵盤來完成需求.
效果如下:
具體需求:
- 當(dāng)焦點(diǎn)在股票價(jià)格編輯框上時, 鍵盤彈出時不能遮蓋住賣出數(shù)量.
即鍵盤彈出是以兩個輸入框底部為基線的. - 鍵盤彈擊要有一個向上推出的動畫效果.
- 兩個輸入框彈出不同的鍵盤界面,
股票價(jià)格輸入框 彈出數(shù)字鍵盤
股票數(shù)量輸入框 彈出數(shù)量鍵盤(如上圖)
最終的實(shí)現(xiàn)效果:
簡書上找了些自定義鍵盤的例子, 基本都不能滿足我的需求, 但是給了我一個很好的切入點(diǎn). 在此非常感謝!
參考其實(shí)現(xiàn), 我做了些封裝. 做了一個自定義鍵盤的工具類,
設(shè)計(jì)原則:與外界充分解耦,通過自定議鍵盤管理者, 綁定對應(yīng)輸入框和鍵盤,鍵盤的實(shí)現(xiàn)者僅需要關(guān)注特殊按鍵的響應(yīng)處理.
設(shè)計(jì)原理:通過傳入activity獲得其DecorView淑趾,添加鍵盤布局。將鍵盤布局set到屏幕底部桥胞,當(dāng)輸入框獲得焦點(diǎn)時,如果設(shè)置了基線view, 則判斷基線view所在位置, 否則默認(rèn)以輸入框?yàn)榛€View考婴,若鍵盤彈出會遮擋基線View,則屏幕整體向上滑動一定的距離:
屏幕移動高度為:
移動距離 = 基線View到屏幕頂部距離 + 自定義鍵盤高度 - 整個屏幕高度
if 移動距離 > 0 則說明當(dāng)鍵盤加入到根布局后, 屏幕無法完成加載, 需要屏幕向上滾動一定的偏移量.
if 移動距離 <= 0 則說明鍵盤彈出后還沒有達(dá)到基線設(shè)置位置, 不需要滾動整個屏幕.
計(jì)算屏幕需要移動的偏移量:
/**
* 計(jì)算屏幕向上移動距離
* @param view 響應(yīng)輸入焦點(diǎn)的控件
* @return 移動偏移量
*/
private int getMoveHeight(View view) {
Rect rect = new Rect();
mRootView.getWindowVisibleDisplayFrame(rect); //獲取當(dāng)前顯示區(qū)域的寬高
int[] vLocation = new int[2];
view.getLocationOnScreen(vLocation); //計(jì)算輸入框在屏幕中的位置
int keyboardTop = vLocation[1] + view.getHeight() + view.getPaddingBottom() + view.getPaddingTop();
if (keyboardTop - mKeyboardHeight < 0) { //如果輸入框到屏幕頂部已經(jīng)不能放下鍵盤的高度, 則不需要移動了.
return 0;
}
if (null != mShowUnderView) { //如果有基線View. 則計(jì)算基線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); //為鍵盤設(shè)置下一個焦點(diǎn)響應(yīng)控件.
refreshKeyboard(keyboard); //設(shè)置鍵盤keyboard到KeyboardView中.
//將鍵盤布局加入到根布局中.
mRootView.addView(mKeyboardViewContainer, mKeyboardViewLayoutParams);
//設(shè)置加載動畫.
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) { //復(fù)原屏幕
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));
}
為了適應(yīng)不同的鍵盤布局, 有必要定義一個Keyboard的基類, 所有的自定義鍵盤都繼承于它. 并且它響應(yīng)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) { //小數(shù)點(diǎn)
if (!editable.toString().contains(".")) {
editable.insert(start, Character.toString((char) primaryCode));
}
} else { //其他默認(rèn)
editable.insert(start, Character.toString((char) primaryCode));
}
}
//getKeyboardView().postInvalidate();
}
public void hideKeyboard() {
//hideSoftKeyboard(etCurrent);
if (null != nextFocusView) nextFocusView.requestFocus();
}
/**
* 處理自定義鍵盤的特殊定制鍵
* 注: 所有的操作要針對etCurrent來操作
*
* @param etCurrent 當(dāng)前操作的EditText
* @param primaryCode 選擇的Key
* @return true: 已經(jīng)處理過, false: 沒有被處理
*/
public abstract boolean handleSpecialKey(EditText etCurrent, int primaryCode);
...... //其它的默認(rèn)空實(shí)現(xiàn)
}
當(dāng)自定義鍵盤時, 僅需要去實(shí)現(xiàn)handleSpecialKey接口, 處理鍵盤中自定義鍵
在BaseKeyboard中已經(jīng)默認(rèn)實(shí)現(xiàn)了基礎(chǔ)的輸入字符, 和 回退, 清空, 隱藏.
當(dāng)然在構(gòu)造時也必須傳入Keyboard所必需的參數(shù) 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); //設(shè)置鍵盤彈出所達(dá)到的基線View
另外在attachTo(editText, baseKeyboard)時, 會設(shè)置editText隱藏系統(tǒng)鍵盤. 設(shè)置其綁定的keyboard, 設(shè)置FocusChangeListener事件監(jiān)聽.
下面是鍵盤布局:
<?xml version="1.0" encoding="UTF-8"?><!-- 數(shù)字鍵盤 -->
<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為了唯一性的原則, 這里將其統(tǒng)一定義在res/values/custom_keyboard.xml中
<!--股票數(shù)量鍵盤-->
<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的樣式的定制. 看遍源碼中, 也沒有找到關(guān)于這些設(shè)置, 有的只是針對KeyboardView的設(shè)置. 但是這些設(shè)置會統(tǒng)一應(yīng)用到所有按鍵上, 還是無法實(shí)現(xiàn)對每個按鍵的獨(dú)立定制樣式.
//源碼中對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);
........
難道以上都白做了么?
...
...
...
經(jīng)過一番細(xì)讀源碼, 決定對KeyboardView進(jìn)行擴(kuò)展.
- 首先Keyboard描述了鍵盤的布局(通過給定的xml),并解析它,
CustomBaseKeyboard及其實(shí)現(xiàn),擴(kuò)展了其對按鍵的處理與EditText的聯(lián)系. - KeyboardView 是承載不同的keyboard并繪制keyboard, 就像是鍵盤布局的繪制板, 并與系統(tǒng)交互.
擴(kuò)展思路:
通過擴(kuò)展的KeyboardView, 對其繪制過程做定制操作, 就可以實(shí)現(xiàn)對每個按鍵樣式的定制了
而KeyboardView的繪制過程并沒有給我們?nèi)魏螜C(jī)會去對其擴(kuò)展定制.
源碼參考
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/inputmethodservice/KeyboardView.java#634
為此只能通過對KeyboardView的重新繪制才能實(shí)現(xiàn).
具體就是重寫onDraw方法, 在onDraw方法中通過接口調(diào)用實(shí)現(xiàn)定制.
并用反射的方法解決需要依賴的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鍵盤進(jìn)行重繪,
// 且CustomBaseKeyboard必需有設(shè)置CustomKeyStyle的回調(diào)接口實(shí)現(xiàn), 才進(jìn)行重繪, 這才有意義
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()進(jìn)行的重寫. 只是在對key的繪制過程中進(jìn)行了重新設(shè)置.
* @param canvas
*/
private void onRefreshKey(Canvas canvas) {
........
//拿到當(dāng)前鍵盤被彈起的輸入源 和 鍵盤為每個key的定制實(shí)現(xiàn)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的默認(rèn)屬性keyBackground設(shè)置
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的默認(rèn)屬性keyTextSize設(shè)置
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的默認(rèn)屬性keyTextColor設(shè)置
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時, 都能進(jìn)行重繪, 在CustomKeyboardManager的attachTo中還要主動為其設(shè)置一個默認(rèn)的實(shí)現(xiàn).
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的樣式設(shè)置
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);
}
});
文中代碼多有省略, 時間倉促且本人能力有限, 僅是對當(dāng)前項(xiàng)目中的實(shí)現(xiàn)做的?定制, 不一定能適用所有的項(xiàng)目, 只是提供了一種參考實(shí)現(xiàn), 相信一定有更好的解決方案, 還請留下你的思路方案, 共同進(jìn)步, 如有缺陷還請留言, 共同解決成長! _
參考:
http://www.reibang.com/p/8fb70cadca27
http://www.reibang.com/p/aedf6f456560
http://931360439-qq-com.iteye.com/blog/938886
具體請參考我的Github
https://github.com/kangqiao182/CustomKeyboard