自定义软键盘-屏蔽系统输入法,随机键盘

  • 核心代码
  • RandomKeyboard
  • RandomKeyboardViewGroup
  • 自定义RandomKeyboard
  • 自定义keyboardViewGroup
  • 其他资源
  • OnCustomKeyDealListener
  • OnKeyboardToggleListener
  • KeyModel.class
  • KEYBOARD_TYPE
  • @KeyboardType
  • random_keyboard_viewgroup.xml
  • styles.xml
  • attrs.xml
  • keyboard_preview.xml
  • bg_key_preview.xml
  • bg_keyboard.xml
  • letter_keyboard_layout.xml
  • letter_num_keyboard_layout.xml
  • num_symbol_keyboard_layout.xml
  • number_keyboard_layout.xml
  • symbol_keyboard_layout.xml
  • 附(KeyboardView,keyboard,Key,Row属性解释)
  • 相关


实现思路:
1.自定义RandomKeyboard,继承系统keyboard
2.自定义keyboardViewGroup(线性布局,含一级子控件NestedScrollView和自定义RandomKeyboard),重新addview方法使添加的子控件添加到NestedScrollView里。可在这个控件初始化时屏蔽系统输入法,监听事件分发通过scrollview中findFocus获取当前有焦点的edittext控件并绑定RandomKeyboard

核心代码

RandomKeyboard
  • 键盘操作
public void onKey(int primaryCode, int[] keyCodes) {
        Editable editable = null;
        int selectionStart = 0;
        if (editText != null) {
            editable = editText.getText();
            selectionStart = editText.getSelectionStart();
        }
        if (keyboard != null) {
            switch (primaryCode) {
                case Keyboard.KEYCODE_SHIFT://shift处理 大小写字母切换
                    keyboard.setShifted(!keyboard.isShifted());
                    invalidateAllKeys();
                    break;
                case Keyboard.KEYCODE_DELETE://删除处理
                    if (editable != null && editable.length() > 0 && selectionStart > 0) {
                        editable.delete(selectionStart - 1, selectionStart);
                    }
                    break;
                   
              ....
              自定义键盘操作
              ....
              
                case ENTER_KEYBOARD://enter处理
                    if (editText != null && editText.getImeOptions() == EditorInfo.IME_ACTION_NEXT) {
                        @SuppressLint("WrongConstant") View view = editText.focusSearch(FOCUS_FORWARD);
                        editText.onEditorAction(EditorInfo.IME_ACTION_NEXT);
                        if (view instanceof EditText) {
                            bindEditText((EditText) view);
                        }
                    } else {
                        if (editText != null && editText.hasFocus()) {
                            editText.clearFocus();
                        }
                        hide();
                    }
                    break;
                default:
                   if (customKeyDealListener == null || !customKeyDealListener.onKey(editText, primaryCode, keyCodes)) {
                        if (primaryCode >= 97 && primaryCode <= 97 + 26) {
                            if (editText != null && editable != null) {
                                editable.insert(selectionStart,
                                        keyboard.isShifted() ? Character.toString((char) (primaryCode - 32))
                                                : Character.toString((char) primaryCode));
                            }
                        } else {
                            if (editable != null) {
                                editable.insert(
                                        selectionStart,
                                        Character.toString((char) primaryCode));
                            }
                        }
                    }
                    break;
            }
        }
    }
  • 随机打乱key
private RandomKeyboard setRandom() {
        if (keyType == TYPE_SYMBOL || keyType == TYPE_LETTER_AND_NUM) {
            return this;
        }
        if (keyboard != null) {
            List<Keyboard.Key> newKeys = keyboard.getKeys();
            if (checkList(newKeys)) {
                containerKeys.clear();
                resultKeys.clear();
                //筛选并缓存可随机的key
                for (Keyboard.Key key : newKeys) {
                    if (key != null && isNeedRandom(key.label)) {
                        containerKeys.add(new KeyModel(key.codes, key.label));
                    }
                }
                //随机打乱筛选出的key
                Random rand = new SecureRandom();
                int size = containerKeys.size();
                for (int i = 0; i < size; i++) {
                    int num = rand.nextInt(size - i);
                    resultKeys.add(containerKeys.get(num));
                    containerKeys.remove(num);
                }
                //将打乱的key赋值给键盘空位(第一步中取值的位置)
                int count = 0;
                for (Keyboard.Key key : newKeys) {
                    if (key != null && isNeedRandom(key.label) && checkListItem(resultKeys, count)) {
                        KeyModel randomKey = resultKeys.get(count);
                        key.label = randomKey.getLable();
                        key.codes = randomKey.getCodes();
                        count++;
                    }
                }
                invalidateAllKeys();
                isRandom = true;
            }
        }
        return this;
    }
  • 禁用系统输入法
//禁用系统输入法
       if (context instanceof Activity) {
            ((Activity) context).getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
        } else {
            Activity currentActivity = AppManager.getAppManager().getCurrentActivity();
            if (currentActivity != null) {
                currentActivity.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
            }
        }
RandomKeyboardViewGroup
  • 自动查找绑定EditText
@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean touchEvent = super.dispatchTouchEvent(ev);
         View editFocus = findEditFocus();

        if (editFocus != null && editFocus.hasFocus()) {
            keyboard.bindEditText((EditText) editFocus);
            keyboard.show();
        } else {
            keyboard.hide();
        }

        if (ViewUtils.isOutsideTheView(editFocus, ev) && ViewUtils.isOutsideTheView(keyboard, ev)) {
            editFocus.clearFocus();
        }
        return touchEvent;
    }

    public View findEditFocus() {
        View currentFocus = slContent != null ? slContent.findFocus() : null;
        return currentFocus instanceof EditText ? currentFocus : null;
    }

自定义RandomKeyboard

继承自KeyboardView
实现OnKeyboardActionListener监听
重写 public void onKey(int primaryCode, int[] keyCodes) 方法控制软键盘操作

public class RandomKeyboard extends KeyboardView implements KeyboardView.OnKeyboardActionListener, LifecycleObserver {

    private static final int LETTER_KEYBOARD = -10;
    private static final int NUMBER_KEYBOARD = -11;
    private static final int SYMBOL_KEYBOARD = -12;
    private static final int ENTER_KEYBOARD = -13;

    private Context context;
    private Keyboard keyboard;
    private EditText editText;

    private boolean isRandom = false;
    private boolean isNeedRandom;
    private int keyType;
    private int defLetterType = TYPE_LETTER;
    private int defNumberType = TYPE_NUMBER;

    private List<KeyModel> containerKeys = new ArrayList<>();
    private List<KeyModel> resultKeys = new ArrayList<>();
    private OnKeyboardToggleListener toggleListener;
    private OnCustomKeyDealListener customKeyDealListener;

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    public void onDestroy() {
        if (containerKeys instanceof ArrayList) {
            containerKeys.clear();
            containerKeys = null;
        }
        if (resultKeys instanceof ArrayList) {
            resultKeys.clear();
            resultKeys = null;
        }
        containerKeys = null;
        resultKeys = null;
        if (context instanceof LifecycleOwner) {
            ((LifecycleOwner) context).getLifecycle().removeObserver(this);
        }
        context = null;
        editText = null;
    }

    public RandomKeyboard(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RandomKeyboard(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        if (context instanceof LifecycleOwner) {
            ((LifecycleOwner) context).getLifecycle().addObserver(this);
        }
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RandomKeyboard);
        isNeedRandom = typedArray.getBoolean(R.styleable.RandomKeyboard_isRandom, false);
        keyType = typedArray.getInt(R.styleable.RandomKeyboard_keyboardType, TYPE_LETTER);
        int resourceId = typedArray.getResourceId(R.styleable.RandomKeyboard_android_keyboardLayout, 0);
        typedArray.recycle();
        setCustomKeyboard(resourceId);
        setOnKeyboardActionListener(this);
        hide();
        //禁用系统输入法
        if (context instanceof Activity) {
            ((Activity) context).getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
        } else {
            Activity currentActivity = AppManager.getAppManager().getCurrentActivity();
            if (currentActivity != null) {
                currentActivity.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
            }
        }
    }

    /**
     * 绑定EditText
     */
    public RandomKeyboard bindEditText(EditText editText) {
        this.editText = editText;
        if (editText != null) {
            int imeOptions = editText.getImeOptions();
            List<Keyboard.Key> keys = CheckValueUtils.checkNotNull(keyboard).getKeys();
            for (int i = 0; i < checkList(keys, "").size(); i++) {
                Keyboard.Key key = keys.get(i);
                if (key.codes[0] == -13) {
                    key.label = imeOptions == EditorInfo.IME_ACTION_NEXT ? "下一步" : "完成";
                    invalidateKey(i);
                    return this;
                }
            }
        }
        return this;
    }

    public RandomKeyboard setCustomKeyboard(@XmlRes int xml) {
        try {
            context.getResources().getXml(xml);
            keyboard = new Keyboard(context, xml);
            setKeyboard(keyboard);
            keyType = TYPE_CUSTOM;
            isRandom(isNeedRandom);
        } catch (Resources.NotFoundException e) {
            setKeyboard(keyType);
        }
        return this;
    }

    /**
     * @see com.quanyou.libraryview.common.Constant.KEYBOARD_TYPE
     */
    public RandomKeyboard setKeyboard(@KeyboardType int type) {
        keyType = type;
        switch (type) {
            case TYPE_LETTER:
                defLetterType = TYPE_LETTER;
                keyboard = new Keyboard(context, R.xml.letter_keyboard_layout);
                break;
            case TYPE_NUMBER:
                defNumberType = TYPE_NUMBER;
                keyboard = new Keyboard(context, R.xml.number_keyboard_layout);
                break;
            case TYPE_SYMBOL:
                keyboard = new Keyboard(context, R.xml.symbol_keyboard_layout);
                break;
            case TYPE_LETTER_AND_NUM:
                defLetterType = TYPE_LETTER_AND_NUM;
                keyboard = new Keyboard(context, R.xml.letter_num_keyboard_layout);
                break;
            case TYPE_NUM_AND_SYMBOL:
                defNumberType = TYPE_NUM_AND_SYMBOL;
                keyboard = new Keyboard(context, R.xml.num_symbol_keyboard_layout);
                break;
            default:
                keyboard = new Keyboard(context, R.xml.letter_keyboard_layout);
                break;
        }
        setKeyboard(keyboard);
        isRandom(isNeedRandom);
        return this;
    }

    public Keyboard getKeyboard() {
        return keyboard;
    }

    @Override
    public void onKey(int primaryCode, int[] keyCodes) {
        Editable editable = null;
        int selectionStart = 0;
        if (editText != null) {
            editable = editText.getText();
            selectionStart = editText.getSelectionStart();
        }
        if (keyboard != null) {
            switch (primaryCode) {
                case Keyboard.KEYCODE_SHIFT://shift处理 大小写字母切换
                    keyboard.setShifted(!keyboard.isShifted());
                    invalidateAllKeys();
                    break;
                case Keyboard.KEYCODE_DELETE://删除处理
                    if (editable != null && editable.length() > 0 && selectionStart > 0) {
                        editable.delete(selectionStart - 1, selectionStart);
                    }
                    break;
                case LETTER_KEYBOARD:
                    setKeyboard(defLetterType);
                    break;
                case NUMBER_KEYBOARD:
                    setKeyboard(defNumberType);
                    break;
                case SYMBOL_KEYBOARD:
                    setKeyboard(TYPE_SYMBOL);
                    break;
                case ENTER_KEYBOARD://enter处理
                    if (editText != null && editText.getImeOptions() == EditorInfo.IME_ACTION_NEXT) {
                        @SuppressLint("WrongConstant") View view = editText.focusSearch(FOCUS_FORWARD);
                        editText.onEditorAction(EditorInfo.IME_ACTION_NEXT);
                        if (view instanceof EditText) {
                            bindEditText((EditText) view);
                        }
                    } else {
                        if (editText != null && editText.hasFocus()) {
                            editText.clearFocus();
                        }
                        hide();
                    }
                    break;
                default:
                    if (customKeyDealListener == null || !customKeyDealListener.onKey(editText, primaryCode, keyCodes)) {
                        if (primaryCode >= 97 && primaryCode <= 97 + 26) {
                            if (editText != null && editable != null) {
                                editable.insert(selectionStart,
                                        keyboard.isShifted() ? Character.toString((char) (primaryCode - 32))
                                                : Character.toString((char) primaryCode));
                            }
                        } else {
                            if (editable != null) {
                                editable.insert(
                                        selectionStart,
                                        Character.toString((char) primaryCode));
                            }
                        }
                    }
                    break;
            }
        }
    }

    @Override
    public void onPress(int primaryCode) {

    }

    @Override
    public void onRelease(int primaryCode) {

    }

    @Override
    public void onText(CharSequence text) {

    }

    @Override
    public void swipeLeft() {

    }

    @Override
    public void swipeRight() {

    }

    @Override
    public void swipeDown() {

    }

    @Override
    public void swipeUp() {

    }

    /**
     * @see #setKeyboard(int)
     */
    @Override
    @Deprecated
    public final void setKeyboard(Keyboard keyboard) {
        isRandom = false;
        super.setKeyboard(keyboard);
    }

    /**
     * 随机打乱key键
     *
     * @see Constant.KEYBOARD_TYPE#TYPE_SYMBOL 字符模式不可用
     * @see Constant.KEYBOARD_TYPE#TYPE_LETTER_AND_NUM 字母数字模式不可用
     */
    public RandomKeyboard isRandom(boolean isRandom) {
        isNeedRandom = isRandom;
        if (this.isRandom == isRandom) {
            return this;
        }
        if (isRandom) {
            setRandom();
        } else {
            setKeyboard(keyType);
        }
        return this;
    }

    public boolean isRandom() {
        return isRandom;
    }

    /**
     * 随机打乱key键
     *
     * @see Constant.KEYBOARD_TYPE#TYPE_SYMBOL 字符模式不可用
     * @see Constant.KEYBOARD_TYPE#TYPE_LETTER_AND_NUM 字母数字模式不可用
     */
    private RandomKeyboard setRandom() {
        if (keyType == TYPE_SYMBOL || keyType == TYPE_LETTER_AND_NUM) {
            return this;
        }
        if (keyboard != null) {
            List<Keyboard.Key> newKeys = keyboard.getKeys();
            if (checkList(newKeys)) {
                containerKeys.clear();
                resultKeys.clear();
                //筛选并缓存可随机的key
                for (Keyboard.Key key : newKeys) {
                    if (key != null && isNeedRandom(key.label)) {
                        containerKeys.add(new KeyModel(key.codes, key.label));
                    }
                }
                //随机打乱筛选出的key
                Random rand = new SecureRandom();
                int size = containerKeys.size();
                for (int i = 0; i < size; i++) {
                    int num = rand.nextInt(size - i);
                    resultKeys.add(containerKeys.get(num));
                    containerKeys.remove(num);
                }
                //将打乱的key赋值给键盘空位(第一步中取值的位置)
                int count = 0;
                for (Keyboard.Key key : newKeys) {
                    if (key != null && isNeedRandom(key.label) && checkListItem(resultKeys, count)) {
                        KeyModel randomKey = resultKeys.get(count);
                        key.label = randomKey.getLable();
                        key.codes = randomKey.getCodes();
                        count++;
                    }
                }
                invalidateAllKeys();
                isRandom = true;
            }
        }
        return this;
    }

    private boolean isNeedRandom(CharSequence s) {
        return !TextUtils.isEmpty(s) && s.length() == 1 && (CheckValueUtils.isNumber(s.toString()) || CheckValueUtils.isLetter(s.toString()));
    }

    public RandomKeyboard show() {
        if (getVisibility() != VISIBLE) {
            setVisibility(VISIBLE);
            if (toggleListener != null) {
                toggleListener.onToggle(true);
            }
        }
        return this;
    }

    public RandomKeyboard hide() {
        if (getVisibility() != GONE || getVisibility() != INVISIBLE) {
            setVisibility(GONE);
            if (toggleListener != null) {
                toggleListener.onToggle(false);
            }
        }
        return this;
    }

    public void setOnKeyboardToggleListener(OnKeyboardToggleListener toggleListener) {
        this.toggleListener = toggleListener;
    }

    public RandomKeyboard setOnCustomKeyDealListener(OnCustomKeyDealListener customKeyDealListener) {
        this.customKeyDealListener = customKeyDealListener;
        return this;
    }
}

自定义keyboardViewGroup

使用方法:在activity布局文件中使用,位置:可做根布局、或置于toolbar之下到根布局底部,可假定为NestedScrollView使用。
注意:
1、keyboardViewGroup中有且只有一个子布局(因这里的布局会加载到NestedScrollView 中)。
2、无需手动绑定EditText,RandomKeyboardViewGroup 自动查找有焦点的EditText并绑定。

public class RandomKeyboardViewGroup extends FrameLayout implements LifecycleObserver {
    private Context context;
    private View inflate;
    private NestedScrollView slContent;
    private RandomKeyboard keyboard;

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    public void onDestroy() {
        if (slContent != null) {
            slContent.removeAllViews();
            slContent = null;
        }
        inflate = null;
        if (context instanceof LifecycleOwner) {
            ((LifecycleOwner) context).getLifecycle().removeObserver(this);
        }
        context = null;
        if (keyboard != null) {
            keyboard.onDestroy();
            keyboard = null;
        }
    }

    public RandomKeyboardViewGroup(Context context) {
        this(context, null);
    }

    public RandomKeyboardViewGroup(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RandomKeyboardViewGroup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        if (context instanceof LifecycleOwner) {
            ((LifecycleOwner) context).getLifecycle().addObserver(this);
        }
        inflate = View.inflate(context, R.layout.random_keyboard_viewgroup, null);
        slContent = ViewUtils.findViewById(inflate, R.id.sl_content);
        keyboard = ViewUtils.findViewById(inflate, R.id.random_keyboard);
        addView(inflate);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RandomKeyboardViewGroup);
        boolean aBoolean = typedArray.getBoolean(R.styleable.RandomKeyboardViewGroup_isRandom, false);
        int anInt = typedArray.getInt(R.styleable.RandomKeyboardViewGroup_keyboardType, TYPE_LETTER);
        int resourceId = typedArray.getResourceId(R.styleable.RandomKeyboard_android_keyboardLayout, 0);
        typedArray.recycle();
        setFocusable(true);
        setFocusableInTouchMode(true);
        try {
            context.getResources().getXml(resourceId);
            keyboard.setCustomKeyboard(resourceId);
        } catch (Resources.NotFoundException e) {
            setKeyboard(anInt);
        }
        isRandom(aBoolean);
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        View editFocus = findEditFocus();
        if (keyboard != null) {
            if (editFocus != null) {
                keyboard.bindEditText((EditText) editFocus);
                keyboard.show();
            } else {
                keyboard.hide();
            }
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean touchEvent = super.dispatchTouchEvent(ev);
        View editFocus = findEditFocus();

        if (editFocus != null && editFocus.hasFocus()) {
            keyboard.bindEditText((EditText) editFocus);
            keyboard.show();
        } else {
            keyboard.hide();
        }

        if (ViewUtils.isOutsideTheView(editFocus, ev) && ViewUtils.isOutsideTheView(keyboard, ev)) {
            editFocus.clearFocus();
        }
        return touchEvent;
    }


    public View findEditFocus() {
        View currentFocus = slContent != null ? slContent.findFocus() : null;
        return currentFocus instanceof EditText ? currentFocus : null;
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if (child.getId() == inflate.getId()) {
            super.addView(child, index, params);
        } else {
            if (slContent != null) {
                slContent.addView(child, index, params);
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ViewGroup.LayoutParams layoutParams = getLayoutParams();
        layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
        layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (inflate != null) {
            ViewGroup.LayoutParams layoutParams1 = inflate.getLayoutParams();
            layoutParams1.width = getMeasuredWidth();
            layoutParams1.height = getMeasuredHeight();
            inflate.setLayoutParams(layoutParams1);
        }
        if (slContent != null && keyboard != null) {
            slContent.setMinimumWidth(getMeasuredHeight() - keyboard.getMeasuredHeight());
        }
    }

    /**
     * 打开关闭软键盘
     */
    public void KeyboardToggle(boolean isShow) {
        if (keyboard != null) {
            if (isShow) {
                keyboard.show();
            } else {
                keyboard.hide();
            }
        }
    }

    /**
     * 打开关闭软键盘
     */
    public void KeyboardToggle(EditText editText) {
        if (keyboard != null && editText != null) {
            keyboard.bindEditText(editText);
            if (editText.hasFocus()) {
                keyboard.show();
            } else {
                keyboard.hide();
            }
        }
    }

    /**
     * @see Constant.KEYBOARD_TYPE
     * @see RandomKeyboard#setKeyboard(int)
     */
    public void setKeyboard(@KeyboardType int type) {
        if (keyboard != null) {
            keyboard.setKeyboard(type);
        }
    }

    /**
     * 随机打乱key-value
     *
     * @see RandomKeyboard#isRandom(boolean)
     */
    public void isRandom(boolean isRandom) {
        if (keyboard != null) {
            keyboard.isRandom(isRandom);
        }
    }

    public void setCustomKeyboard(@XmlRes int xml) {
        if (keyboard != null) {
            keyboard.setCustomKeyboard(xml);
        }
    }

    public void setOnCustomKeyDealListener(OnCustomKeyDealListener customKeyDealListener) {
        if (keyboard != null) {
            keyboard.setOnCustomKeyDealListener(customKeyDealListener);
        }
    }

    public boolean isRandom() {
        return keyboard != null && keyboard.isRandom();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            if (null != keyboard && keyboard.getVisibility() == VISIBLE) {
                keyboard.hide();
                return true;
            }
        }
        return super.onKeyDown(keyCode, event);
    }

    public void setOnKeyboardToggleListener(OnKeyboardToggleListener toggleListener) {
        if (keyboard != null) {
            keyboard.setOnKeyboardToggleListener(toggleListener);
        }
    }
}

其他资源

OnCustomKeyDealListener
public interface OnCustomKeyDealListener {
    boolean onKey(EditText editText, int primaryCode, int[] keyCodes);
}
OnKeyboardToggleListener
public interface OnKeyboardToggleListener {
    void onToggle(boolean isOpen);
}
KeyModel.class
data class KeyModel(var codes: IntArray, var lable: CharSequence)
KEYBOARD_TYPE
public interface Constant {
    interface KEYBOARD_TYPE {
        int TYPE_LETTER = 1;
        int TYPE_NUMBER = 2;
        int TYPE_SYMBOL = 3;
        int TYPE_LETTER_AND_NUM = 4;
        int TYPE_NUM_AND_SYMBOL = 5;
    }
}
@KeyboardType
@IntDef(
    value = [KEYBOARD_TYPE.TYPE_LETTER,
        KEYBOARD_TYPE.TYPE_NUMBER,
        KEYBOARD_TYPE.TYPE_SYMBOL,
        KEYBOARD_TYPE.TYPE_LETTER_AND_NUM,
        KEYBOARD_TYPE.TYPE_NUM_AND_SYMBOL]
)
@Retention(AnnotationRetention.SOURCE)
annotation class KeyboardType
random_keyboard_viewgroup.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/random_keyboard_group"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.core.widget.NestedScrollView
        android:id="@+id/sl_content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <com.*.widget.RandomKeyboard
        android:id="@+id/random_keyboard"
        style="@style/RandomKeyboard"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
styles.xml
<style name="RandomKeyboardParent">
        <item name="android:background">#212121</item>
        <item name="android:keyBackground">@drawable/bg_keyboard</item>
        <item name="android:keyPreviewHeight">50dp</item>
        <item name="android:keyPreviewLayout">@layout/keyboard_preview</item>
        <item name="android:keyPreviewOffset">-20dp</item>
        <item name="android:keyTextColor">#dddddd</item>
        <item name="android:keyTextSize">18sp</item>
        <item name="android:labelTextSize">18sp</item>
        <item name="android:paddingTop">2dp</item>
        <item name="android:paddingBottom">2dp</item>
    </style>

    <style name="RandomKeyboard" parent="RandomKeyboardParent" />
attrs.xml
<attr name="isRandom" format="boolean" />
    <attr name="keyboardType" format="enum">
        <enum name="TYPE_LETTER" value="1" />
        <enum name="TYPE_NUMBER" value="2" />
        <enum name="TYPE_SYMBOL" value="3" />
        <enum name="TYPE_LETTER_AND_NUM" value="4" />
        <enum name="TYPE_NUM_AND_SYMBOL" value="5" />
    </attr>

   <declare-styleable name="RandomKeyboard">
        <attr name="isRandom" />
        <attr name="keyboardType" />
        <attr name="android:keyboardLayout"/>
    </declare-styleable>

    <declare-styleable name="RandomKeyboardViewGroup">
        <attr name="isRandom" />
        <attr name="keyboardType" />
        <attr name="android:keyboardLayout"/>
    </declare-styleable>
keyboard_preview.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_key_preview"
    android:gravity="center"
    android:padding="5dp"
    android:textColor="#4D4D4D"
    android:textSize="20sp"
    android:textStyle="bold"/>
bg_key_preview.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="4dp" />
    <solid android:color="@color/subColorWhite" />
    <stroke
        android:width="1px"
        android:color="@color/gray" />
</shape>
bg_keyboard.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:bottom="2dp"
        android:left="2dp"
        android:right="2dp"
        android:top="2dp">
        <selector>
            <!-- 按压后图层 -->
            <item android:state_pressed="true">
                <shape>
                    <solid android:color="#565656" />
                    <corners android:radius="5dp" />
                </shape>
            </item>
            <!-- 正常状态图层 -->
            <item>
                <shape>
                    <solid android:color="#383838" />
                    <corners android:radius="5dp" />
                </shape>
            </item>
        </selector>
    </item>
</layer-list>
letter_keyboard_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:keyHeight="50dp"
    android:keyWidth="10%p">
    <Row android:rowEdgeFlags="top">
        <Key
            android:keyEdgeFlags="left"
            android:keyLabel="q" />
        <Key android:keyLabel="w" />
        <Key android:keyLabel="e" />
        <Key android:keyLabel="r" />
        <Key android:keyLabel="t" />
        <Key android:keyLabel="y" />
        <Key android:keyLabel="u" />
        <Key android:keyLabel="i" />
        <Key android:keyLabel="o" />
        <Key
            android:keyEdgeFlags="right"
            android:keyLabel="p" />
    </Row>
    <Row>
        <Key
            android:horizontalGap="5%p"
            android:keyEdgeFlags="left"
            android:keyLabel="a" />
        <Key android:keyLabel="s" />
        <Key android:keyLabel="d" />
        <Key android:keyLabel="f" />
        <Key android:keyLabel="g" />
        <Key android:keyLabel="h" />
        <Key android:keyLabel="j" />
        <Key android:keyLabel="k" />
        <Key
            android:keyEdgeFlags="right"
            android:keyLabel="l" />
    </Row>
    <Row>
        <Key
            android:codes="-1"
            android:isModifier="true"
            android:isSticky="true"
            android:keyEdgeFlags="left"
            android:keyLabel="shift"
            android:keyWidth="15%p" />
        <Key android:keyLabel="z" />
        <Key android:keyLabel="x" />
        <Key android:keyLabel="c" />
        <Key android:keyLabel="v" />
        <Key android:keyLabel="b" />
        <Key android:keyLabel="n" />
        <Key android:keyLabel="m" />

        <Key
            android:codes="-5"
            android:isModifier="true"
            android:isRepeatable="true"
            android:keyEdgeFlags="right"
            android:keyLabel="delete"
            android:keyWidth="15%p" />
    </Row>

    <Row android:rowEdgeFlags="bottom">
        <Key
            android:codes="-11"
            android:keyEdgeFlags="left"
            android:keyLabel="123"
            android:keyWidth="15%p" />
        <Key
            android:codes="64"
            android:keyLabel="\@"/>
        <Key
            android:codes="32"
            android:isRepeatable="true"
            android:keyWidth="35%p"
            android:keyLabel="space" />

        <Key android:keyLabel="." />

        <Key
            android:codes="-12"
            android:keyWidth="15%p"
            android:keyLabel="!?#" />
        <Key
            android:codes="-13"
            android:keyWidth="15%p"
            android:keyEdgeFlags="right"
            android:keyLabel="完成" />
    </Row>
</Keyboard>
letter_num_keyboard_layout.xml
<?xml version="1.0" encoding="UTF-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:keyWidth="10%p"
    android:keyHeight="50dp">

    <Row android:rowEdgeFlags="top">
        <Key
            android:keyEdgeFlags="left"
            android:keyLabel="1" />
        <Key android:keyLabel="2" />
        <Key android:keyLabel="3" />
        <Key android:keyLabel="4" />
        <Key android:keyLabel="5" />
        <Key android:keyLabel="6" />
        <Key android:keyLabel="7" />
        <Key android:keyLabel="8" />
        <Key android:keyLabel="9" />
        <Key android:keyEdgeFlags="right"
            android:keyLabel="0" />
    </Row>

    <Row>
        <Key
            android:keyEdgeFlags="left"
            android:keyLabel="q" />
        <Key android:keyLabel="w" />
        <Key android:keyLabel="e" />
        <Key android:keyLabel="r" />
        <Key android:keyLabel="t" />
        <Key android:keyLabel="y" />
        <Key android:keyLabel="u" />
        <Key android:keyLabel="i" />
        <Key android:keyLabel="o" />
        <Key
            android:keyEdgeFlags="right"
            android:keyLabel="p" />
    </Row>
    <Row>
        <Key
            android:horizontalGap="5%p"
            android:keyEdgeFlags="left"
            android:keyLabel="a" />
        <Key android:keyLabel="s" />
        <Key android:keyLabel="d" />
        <Key android:keyLabel="f" />
        <Key android:keyLabel="g" />
        <Key android:keyLabel="h" />
        <Key android:keyLabel="j" />
        <Key android:keyLabel="k" />
        <Key
            android:keyEdgeFlags="right"
            android:keyLabel="l" />
    </Row>
    <Row>
        <Key
            android:codes="-1"
            android:isModifier="true"
            android:isSticky="true"
            android:keyWidth="15%p"
            android:keyHeight="100dp"
            android:keyEdgeFlags="left"
            android:keyLabel="shift" />
        <Key android:keyLabel="z" />
        <Key android:keyLabel="x" />
        <Key android:keyLabel="c" />
        <Key android:keyLabel="v" />
        <Key android:keyLabel="b" />
        <Key android:keyLabel="n" />
        <Key android:keyLabel="m" />

        <Key
            android:codes="-5"
            android:isModifier="true"
            android:isRepeatable="true"
            android:keyWidth="15%p"
            android:keyEdgeFlags="right"
            android:keyLabel="delete" />
    </Row>

    <Row android:rowEdgeFlags="bottom">

        <Key
            android:codes="64"
            android:horizontalGap="15%p"
            android:keyWidth="15%p"
            android:keyLabel="\@"/>

        <Key
            android:codes="32"
            android:isRepeatable="true"
            android:keyWidth="40%p"
            android:keyLabel="space" />

        <Key
            android:codes="-12"
            android:keyWidth="15%p"
            android:keyLabel="!?#" />

        <Key
            android:codes="-13"
            android:isRepeatable="true"
            android:keyWidth="15%p"
            android:keyEdgeFlags="right"
            android:keyLabel="完成" />
    </Row>
</Keyboard>
num_symbol_keyboard_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:horizontalGap="0.9%p"
    android:keyWidth="9%p"
    android:keyHeight="52dp"
    android:verticalGap="0px">

    <Row>
        <Key
            android:codes="49"
            android:keyLabel="1" />
        <Key
            android:codes="50"
            android:keyLabel="2" />
        <Key
            android:codes="51"
            android:keyLabel="3" />
        <Key
            android:codes="52"
            android:keyLabel="4" />
        <Key
            android:codes="53"
            android:keyLabel="5" />
        <Key
            android:codes="54"
            android:keyLabel="6" />

        <Key
            android:codes="55"
            android:keyLabel="7" />
        <Key
            android:codes="56"
            android:keyLabel="8" />


        <Key
            android:codes="57"
            android:keyLabel="9" />
        <Key
            android:codes="48"
            android:keyLabel="0" />
    </Row>

    <Row>
        <Key
            android:codes="45"
            android:keyLabel="-" />
        <Key
            android:codes="47"
            android:keyLabel="/" />
        <Key
            android:codes="58"
            android:keyLabel=":" />
        <Key
            android:codes="59"
            android:keyLabel=";" />
        <Key
            android:codes="40"
            android:keyLabel="(" />
        <Key
            android:codes="41"
            android:keyLabel=")" />
        <Key
            android:codes="36"
            android:keyLabel="$" />
        <!--&-->
        <Key
            android:codes="38"
            android:keyLabel="&" />
        <Key
            android:codes="64"
            android:keyLabel="\@" />
        <Key
            android:codes="34"
            android:keyLabel=""" />
    </Row>

    <Row>
        <Key
            android:codes="-12"
            android:keyWidth="11.5%p"
            android:keyEdgeFlags="left"
            android:keyLabel="!?#" />
        <Key
            android:codes="46"
            android:keyLabel="." />
        <Key
            android:codes="44"
            android:keyLabel="," />
        <Key
            android:codes="63"
            android:keyLabel="\?" />
        <Key
            android:codes="33"
            android:keyLabel="!" />
        <Key
            android:codes="39"
            android:keyLabel="'" />
        <Key
            android:codes="42"
            android:keyLabel="*" />
        <Key
            android:codes="43"
            android:keyLabel="+" />
        <Key
            android:codes="-5"
            android:isRepeatable="true"
            android:keyWidth="16.5%p"
            android:keyEdgeFlags="right"
            android:keyLabel="delete" />
    </Row>

    <Row android:rowEdgeFlags="bottom">
        <Key
            android:codes="-10"
            android:keyWidth="22%p"
            android:keyLabel="ABC" />
        <Key
            android:codes="32"
            android:isRepeatable="true"
            android:keyWidth="50.4%p"
            android:keyLabel="space" />
        <Key
            android:codes="-13"
            android:keyWidth="24%p"
            android:keyEdgeFlags="right"
            android:keyLabel="完成" />
    </Row>
</Keyboard>
number_keyboard_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:keyWidth="25%p"
    android:keyHeight="50dp">
    <Row>
        <Key
            android:keyEdgeFlags="left"
            android:keyLabel="1" />
        <Key android:keyLabel="2" />
        <Key android:keyLabel="3" />
        <Key
            android:codes="-5"
            android:isModifier="true"
            android:isRepeatable="true"
            android:keyEdgeFlags="right"
            android:keyLabel="delete" />
    </Row>
    <Row>
        <Key
            android:keyEdgeFlags="left"
            android:keyLabel="4" />
        <Key android:keyLabel="5" />
        <Key android:keyLabel="6" />
        <Key
            android:codes="-12"
            android:keyEdgeFlags="right"
            android:keyLabel="!?#" />
    </Row>
    <Row>
        <Key
            android:keyEdgeFlags="left"
            android:keyLabel="7" />
        <Key android:keyLabel="8" />
        <Key android:keyLabel="9" />
        <Key
            android:keyEdgeFlags="right"
            android:keyLabel="." />
    </Row>
    <Row android:rowEdgeFlags="bottom">
        <Key
            android:codes="-10"
            android:keyWidth="25%p"
            android:keyEdgeFlags="left"
            android:keyLabel="ABC" />
        <Key
            android:codes="48"
            android:keyWidth="50%p"
            android:keyLabel="0" />
        <Key
            android:codes="-13"
            android:keyWidth="25%p"
            android:keyEdgeFlags="right"
            android:keyLabel="完成" />
    </Row>
</Keyboard>
symbol_keyboard_layout.xml
<?xml version="1.0" encoding="UTF-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:keyHeight="52dp"
    android:keyWidth="9%p"
    android:horizontalGap="0.9%p"
    android:verticalGap="0px">
    <Row>
        <Key
            android:codes="91"
            android:keyEdgeFlags="left"
            android:keyLabel="["/>
        <Key
            android:codes="93"
            android:keyLabel="]"/>
        <Key
            android:codes="123"
            android:keyLabel="{"/>
        <Key
            android:codes="125"
            android:keyLabel="}"/>
        <Key
            android:codes="35"
            android:keyLabel="#"/>
        <Key
            android:codes="37"
            android:keyLabel="%"/>
        <Key
            android:codes="94"
            android:keyLabel="^"/>
        <Key
            android:codes="42"
            android:keyLabel="*"/>
        <Key
            android:codes="43"
            android:keyLabel="+"/>
        <Key
            android:codes="61"
            android:keyEdgeFlags="right"
            android:keyLabel="="/>
    </Row>
    <Row>
        <Key
            android:codes="95"
            android:keyEdgeFlags="left"
            android:keyLabel="_"/>
        <Key
            android:codes="92"
            android:keyLabel="\\"/>
        <Key
            android:codes="124"
            android:keyLabel="|"/>
        <Key
            android:codes="126"
            android:keyLabel="~"/>

        <!--<-->
        <Key
            android:codes="60"
            android:keyLabel="<"/>
        <!-->-->
        <Key
            android:codes="62"
            android:keyLabel=">"/>
        <Key
            android:codes="8364"
            android:keyLabel="€"/>
        <Key
            android:codes="163"
            android:keyLabel="£"/>
        <Key
            android:codes="165"
            android:keyLabel="¥"/>

        <!-- todo • -->
        <Key
            android:codes="8226"
            android:keyLabel="•"/>
    </Row>

    <Row>
        <Key
            android:codes="-11"
            android:keyEdgeFlags="left"
            android:keyLabel="123"
            android:keyWidth="16.9%p" />
        <Key
            android:codes="45"
            android:keyLabel="-" />
        <Key
            android:codes="46"
            android:keyLabel="." />
        <Key
            android:codes="44"
            android:keyLabel="," />
        <Key
            android:codes="63"
            android:keyLabel="\?" />
        <Key
            android:codes="33"
            android:keyLabel="!" />
        <Key
            android:codes="39"
            android:keyLabel="'" />
        <Key
            android:codes="-5"
            android:isRepeatable="true"
            android:keyWidth="21%p"
            android:keyEdgeFlags="right"
            android:keyLabel="delete" />
    </Row>

    <Row android:rowEdgeFlags="bottom">
        <Key
            android:codes="-10"
            android:keyWidth="22%p"
            android:keyLabel="ABC" />
        <Key
            android:codes="32"
            android:isRepeatable="true"
            android:keyWidth="50.4%p"
            android:keyLabel="space" />
        <Key
            android:codes="-13"
            android:keyWidth="24%p"
            android:keyEdgeFlags="right"
            android:keyLabel="完成" />
    </Row>

</Keyboard>

附(KeyboardView,keyboard,Key,Row属性解释)

  • Keyboard

参数

值类型

释义

传值

keyHeight

dimension/fractional

key高度

精确值(dp、px等)和相对值(%、%p)

keyWidth

dimension/fractional

key宽度

同上

horizontalGap

dimension/fractional

key间水平间隙

同上

verticalGap

dimension/fractional

key间垂直间隙

同上

  • Row

参数

值类型

释义

传值

keyHeight

dimension/fractional

key高度

精确值(dp、px等)和相对值(%、%p)

keyWidth

dimension/fractional

key宽度

同上

horizontalGap

dimension/fractional

key间水平间隙

同上

verticalGap

dimension/fractional

key间垂直间隙

同上

keyboardMode

reference

键盘模式,不符该模式则跳过此行

rowEdgeFlags

enum

行边界标记

top/bottom,键盘顶部和底部

  • Key

参数

值类型

释义

传值

keyHeight

dimension/fractional

key高度

精确值(dp、px等)和相对值(%、%p)

keyWidth

dimension/fractional

key宽度

同上

horizontalGap

dimension/fractional

key间水平间隙

同上

verticalGap

dimension/fractional

key间垂直间隙

同上

keyEdgeFlags

enum

Key边缘位置标记

left/right,键盘左边和右边

codes

int

Key输出符号对应的Unicode值

iconPreview

reference

弹出回显的控件

布局资源

isModifier

boolean

是否为功能键,如:Alt/Shift

isRepeatable

boolean

是否允许长按重复执行

isSticky

boolean

是否是开关键,例如Shift大小写切换按键,具有两种状态,按下状态和正常状态,取值为true或则false。

keyIcon

reference

替换label显示在按键上的图标

图片资源

keyLabel

reference

显示在Key上的标签

字符

keyOutputText

string

Key按下时输出的字符或字符串

popupCharacters

string

小键盘显示的字符,用于显示Key候选项。

popupKeyboard

reference

按键候选小键盘的keyboard布局

布局资源

  • KeyboardView

参数

值类型

释义

传值

keyBackground

reference

按键的图像背景,必须包含多个状态的drawable

verticalCorrection

dimension

补充触摸y坐标的偏移,用于偏差矫正

keyPreviewLayout

reference

按键按下时预览框的布局

keyPreviewOffset

dimension

按键按下时预览框的偏移。>0 向下,<0 向上。

keyPreviewHeight

dimension

按键按下时预览框的高度。

keyTextSize

dimension

按键文字大小。

keyTextColor

color

按键文字颜色。

labelTextSize

dimension

标签文字大小,keylabel有多个字符且keycodes只有一个值时,该属性生效。

popupLayout

reference

按键候选小键盘的KeyboardView布局。

shadowRadius

float

按键文字阴影半径

shadowColor

color

按键文字阴影颜色