如何在一个视图中捕获软键盘输入?

55
我有一个子类视图,当它在onTouchEvent接收到“touch up”时会弹出键盘。 它通过请求焦点,检索InputMethodManager,然后调用showSoftInput来显示这个操作。
现在我需要找出如何捕获软键盘上按下的字母,因为它们被按下。 我当前仅在按下软键盘上的“下一个/完成”按钮时才得到响应。
以下是我的类:
public class BigGrid extends View {

    private static final String TAG = "BigGrid";

    public BigGrid(Context context) {
        super(context);
        setFocusableInTouchMode(true); // allows the keyboard to pop up on
                                       // touch down

        setOnKeyListener(new OnKeyListener() {
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                Log.d(TAG, "onKeyListener");
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    // Perform action on key press
                    Log.d(TAG, "ACTION_DOWN");
                    return true;
                }
                return false;
            }
        });
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        Log.d(TAG, "onTOUCH");
        if (event.getAction() == MotionEvent.ACTION_UP) {

            // show the keyboard so we can enter text
            InputMethodManager imm = (InputMethodManager) getContext()
                    .getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.showSoftInput(this, InputMethodManager.SHOW_FORCED);
        }
        return true;
    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        Log.d(TAG, "onCreateInputConnection");

        BaseInputConnection fic = new BaseInputConnection(this, true);
        outAttrs.actionLabel = null;
        outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
        outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
        return fic;
    }

    @Override
    public boolean onCheckIsTextEditor() {
        Log.d(TAG, "onCheckIsTextEditor");
        return true;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawColor(R.color.grid_bg);
        // .
        // .
        // alot more drawing code...
        // .
    }
}

键盘弹出了,但我的onKeyListener只在我按下键盘上的“Next”按钮时才触发。我需要知道哪个字符被点击了,以便我可以在我的onDraw()方法中显示它。


扩展TextView不是更好吗? - pawelzieba
1
扩展TextView并不能让我对监听按键事件有更多的控制权(至少我不知道),但它确实提供了一些特定的样式和文本绘制,这些我只需要覆盖并消除即可。这就是为什么我选择子类化View的原因。 - rich.e
4个回答

47

实际上,您可以在不从TextView派生视图的情况下自己处理关键事件。

要做到这一点,只需按照以下方式修改原始代码:

1)在onCreateInputConnection()中替换以下行:

outAttrs.inputType = InputType.TYPE_CLASS_TEXT;

用这个:
outAttrs.inputType = InputType.TYPE_NULL;

基于InputType.TYPE_NULL的文档说明:"应该将其解释为目标输入连接不是丰富的,它无法处理和显示诸如候选文本之类的内容,也无法检索当前文本,因此输入法将需要在有限的“生成键事件”模式下运行。"
2) 在同一方法中替换以下行:
BaseInputConnection fic = new BaseInputConnection(this, true);

用这个:
BaseInputConnection fic = new BaseInputConnection(this, false);

第二个假参数会将BaseInputConnection置于“虚拟”模式,这也是为了使原始按键事件发送到您的视图所必需的。在BaseInputConnection代码中,您可以找到几个注释,例如:“仅在虚拟模式下,才会为新文本发送按键事件并清除当前可编辑缓冲区。”
我已经使用了这种方法,让软键盘向我的一个视图发送原始事件,该视图派生自LinearLayout(即不派生自TextView的视图),我可以确认它有效。
当然,如果您不需要设置IME_ACTION_DONE imeOptions值以在键盘上显示完成按钮,则可以完全删除onCreateInputConnection()和onCheckIsTextEditor()覆盖,因为没有定义更复杂处理能力的输入连接,原始事件将默认发送到您的视图。但不幸的是,似乎没有简单的方法来配置EditorInfo属性,而无需重写这些方法并提供BaseInputConnection对象,一旦这样做,如果要再次接收原始按键事件,则必须像上面描述的那样降低该对象执行的处理。
警告:在某些最近版本的默认LatinIME键盘中引入了两个错误,该键盘随Android(Google Keyboard)一起出货,当使用该键盘时,可能会影响键盘事件处理(如上所述)。我已经在应用程序端设计了一些解决方法,并提供了示例代码,似乎可以避免这些问题。要查看这些解决方法,请参见以下答案:

Android - cannot capture backspace/delete press in soft. keyboard


2
对于那些感兴趣的人,我现在正在处理的一个问题是自定义键盘不遵守 InputType.TYPE_NULL。具体来说,Minuum键盘似乎完全忽略了这个设置,这破坏了我的应用程序。 - Melllvar
此外,非常重要的是,随着更近期的三星设备分发的三星键盘未能正确实现此设置。具体而言,当使用TYPE_NULL时,它未能抑制预测文本。如果您关闭预测文本,则可以正常使用TYPE_NULL,但当然应用程序用户通常不会意识到这一点。 - Carl
隐藏键盘的代码:imm.hideSoftInputFromWindow(this.getWindowToken(), 0);。虽然有很多建议,但这是唯一对我有效的解决方案。 - JoKr

27
根据文档View(编辑器)通过InputConnection从键盘(IME)接收命令,并通过InputMethodManager向键盘发送命令。

enter image description here

我将在下面展示整个代码,但是这里有一些步骤。

1. 让键盘出现

由于视图正在向键盘发送命令,因此它需要使用一个 InputMethodManager 。为了方便起见,我们将假设当视图被点击时,它将显示键盘(如果已经显示则隐藏它)。

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_UP) {
        InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY);
    }
    return true;
}

视图还需要先调用setFocusableInTouchMode(true)

2. 从键盘接收输入

为了让视图从键盘接收输入,它需要重写onCreateInputConnection()。这将返回键盘用于与视图通信的InputConnection
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
    outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
    return new MyInputConnection(this, true);
}

outAttrs 指定视图请求的键盘类型。在这里,我们只是请求普通文本键盘。选择 TYPE_CLASS_NUMBER 将显示数字键盘(如果有)。还有许多其他选项。请参见 EditorInfo

您必须返回一个 InputConnection,通常是 BaseInputConnection 的自定义子类。在该子类中,您提供对可编辑字符串的引用,键盘将对其进行更新。由于 SpannableStringBuilder 实现了 Editable 接口,因此我们将在基本示例中使用它。

public class MyInputConnection extends BaseInputConnection {

    private SpannableStringBuilder mEditable;

    MyInputConnection(View targetView, boolean fullEditor) {
        super(targetView, fullEditor);
        MyCustomView customView = (MyCustomView) targetView;
        mEditable = customView.mText;
    }

    @Override
    public Editable getEditable() {
        return mEditable;
    }
}

我们在这里所做的只是将输入连接与自定义视图中的文本变量BaseInputConnection关联起来。这将由{{link1}}来处理编辑mText。这可能就是你需要做的全部。然而,你可以查看源代码并查看哪些方法说“默认实现”,特别是“默认实现不执行任何操作”。这些都是你可能想要覆盖的其他方法,具体取决于你的编辑器视图的复杂程度。你还应该浏览文档中的所有方法名称。其中一些有针对“编辑器作者”的注释。请特别注意这些注释。

有些键盘出于某些原因(例如删除、回车和一些数字键),不会通过InputConnection发送某些输入。对于这些键,我添加了一个OnKeyListener。在五个不同的软键盘上测试这个设置,一切似乎都能正常工作。相关的补充答案在这里:

完整项目代码

以下是我完整的示例供参考。

enter image description here

MyCustomView.java

public class MyCustomView extends View {

    SpannableStringBuilder mText;

    public MyCustomView(Context context) {
        this(context, null, 0);
    }

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

    public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setFocusableInTouchMode(true);
        mText = new SpannableStringBuilder();

        // handle key presses not handled by the InputConnection
        setOnKeyListener(new OnKeyListener() {
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_DOWN) {

                    if (event.getUnicodeChar() == 0) { // control character

                        if (keyCode == KeyEvent.KEYCODE_DEL) {
                            mText.delete(mText.length() - 1, mText.length());
                            Log.i("TAG", "text: " + mText + " (keycode)");
                            return true;
                        }
                        // TODO handle any other control keys here
                    } else { // text character
                        mText.append((char)event.getUnicodeChar());
                        Log.i("TAG", "text: " + mText + " (keycode)");
                        return true;
                    }
                }
                return false;
            }
        });
    }

    // toggle whether the keyboard is showing when the view is clicked
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP) {
            InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY);
        }
        return true;
    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
        // outAttrs.inputType = InputType.TYPE_CLASS_NUMBER; // alternate (show number pad rather than text)
        return new MyInputConnection(this, true);
    }
}

MyInputConnection.java

public class MyInputConnection extends BaseInputConnection {

    private SpannableStringBuilder mEditable;

    MyInputConnection(View targetView, boolean fullEditor) {
        super(targetView, fullEditor);
        MyCustomView customView = (MyCustomView) targetView;
        mEditable = customView.mText;
    }

    @Override
    public Editable getEditable() {
        return mEditable;
    }

    // just adding this to show that text is being committed.
    @Override
    public boolean commitText(CharSequence text, int newCursorPosition) {
        boolean returnValue = super.commitText(text, newCursorPosition);
        Log.i("TAG", "text: " + mEditable);
        return returnValue;
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.editorview.MainActivity">

    <com.example.editorview.MyCustomView
        android:id="@+id/myCustomView"
        android:background="@android:color/holo_blue_bright"
        android:layout_margin="50dp"
        android:layout_width="300dp"
        android:layout_height="150dp"
        android:layout_centerHorizontal="true"
        />

</RelativeLayout>

MainActivity.java代码中没有什么特别的。

如果这个方法对你不起作用,请留下评论。我正在制作一个自定义EditText的库,使用这种基本解决方案,如果有任何边缘情况无法正常工作,我想知道。如果您想查看该项目,自定义视图在这里。它的InputConnection这里

相关信息


这段代码运行良好,除了数字输入。CommitText被用于字母和符号,但不适用于数字。我只是取消注释了带有TYPE_CLASS_NUMBER的行,并注释了TYPE_CLASS_TEXT。有人知道为什么它不起作用吗? - Serhiy
1
@Suragch,谢谢,使用mEditable而不是回调函数是个好主意。我还发现,在数字输入时会调用sendKeyEvent而不是commitText。 - Serhiy
@Serhii,你用哪个键盘测试这个?我也应该试一下。 - Suragch
@Suragch,使用标准软键盘 - Serhiy

23

事实证明,我确实需要子类化TextView并使用addTextChangedListener() 来添加自己的TextWatcher实现,以便监听软键盘事件。 我找不到用普通的View做到这一点的方法。

还有一件事,对于那些尝试这种技术的人; TextView默认无法编辑文本,因此如果您想使自己的实现可编辑(而不是子类化EditText,我不想这样做),您还必须制作一个自定义InputConnection,例如以下内容:

 /**
 * MyInputConnection
 * BaseInputConnection configured to be editable
 */
class MyInputConnection extends BaseInputConnection {
    private SpannableStringBuilder _editable;
    TextView _textView;

    public MyInputConnection(View targetView, boolean fullEditor) {
        super(targetView, fullEditor);
        _textView = (TextView) targetView;
    }

    public Editable getEditable() {
        if (_editable == null) {
            _editable = (SpannableStringBuilder) Editable.Factory.getInstance()
            .newEditable("Placeholder");
        }
        return _editable;
    }

    public boolean commitText(CharSequence text, int newCursorPosition) {
        _editable.append(text);
        _textView.setText(text);
        return true;
    }
}

然后您可以使用以下内容覆盖 onCheckisTextEditor 和 onCreateInputConnection:

 @Override
 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
     outAttrs.actionLabel = null;
     outAttrs.label = "Test text";
     outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
     outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;

     return new MyInputConnection(this, true);
 }

 @Override
 public boolean onCheckIsTextEditor() {
     return true;
 }

完成这些步骤后,您将拥有一个可以监听软键盘的视图,并且可以对按键输入值进行任何操作。


虽然在大多数情况下这可能是有效的,但我最近从LatinIME开发人员中了解到,给定的IME可以选择通过调用除commitText()之外的其他方法向可编辑文本添加文本。例如,IME可以使用commitCompletion(),它将扩展当前的组合文本。因此,您可能需要研究一些额外的方法覆盖,尽管您的一般方法可能非常好。 - Carl

4
我的理解是您的onKeyListener只会接收硬件键盘按键事件。

如果您重写boolean View.onKeyPreIme(int keyCode, KeyEvent event),您将获得所有输入按键事件。

这样,您可以选择处理按键事件的操作[DOWN | MULTIPLE | UP]并返回true,或者允许正常的按键处理来处理它(return super.onKeyPreIme())。

1
我仍然需要尝试onKeyPreIme,它看起来可能会给我我想要的东西。但是,使用TextWatcher肯定有效。 - rich.e

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接