How to capture soft keyboard input in a View?

Android

Android Problem Overview


I have a subclassed View that pops up the keyboard when it receives a 'touch up' in onTouchEvent. It shows this by requesting focus, retrieving the InputMethodManager, and then calling showSoftInput.

Now I need to figure out how to capture the tapped letters of the soft keyboard, as they are pressed. I am currently only getting a response when the Next/Done button is pressed on the soft keyboard.

Here is my class:

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...
		// .
	}
}

The keyboard shows, but my onKeyListener only fires when I press the 'Next' button on the keyboard. I need which character is tapped, so that I can display it in my onDraw() method.

Android Solutions


Solution 1 - Android

It is actually possible to handle the key events yourself without deriving your view from TextView.

To do this, just modify your original code as follows:

  1. Replace the following line in onCreateInputConnection():

    outAttrs.inputType = InputType.TYPE_CLASS_TEXT;

with this one:

outAttrs.inputType = InputType.TYPE_NULL;

Per the documentation for InputType.TYPE_NULL: "This should be interpreted to mean that the target input connection is not rich, it can not process and show things like candidate text nor retrieve the current text, so the input method will need to run in a limited 'generate key events' mode."

  1. Replace the following line in the same method:

    BaseInputConnection fic = new BaseInputConnection(this, true);

with this one:

BaseInputConnection fic = new BaseInputConnection(this, false);

The false second argument puts the BaseInputConnection into "dummy" mode, which is also required in order for the raw key events to be sent to your view. In the BaseInputConnection code, you can find several comments such as the following: "only if dummy mode, a key event is sent for the new text and the current editable buffer cleared."

I have used this approach to have the soft keyboard send raw events to a view of mine that is derived from LinearLayout (i.e., a view not derived from TextView), and can confirm that it works.

Of course, if you didn't need to set the IME_ACTION_DONE imeOptions value to show a Done button on the keyboard, then you could just remove the onCreateInputConnection() and onCheckIsTextEditor() overrides entirely, and raw events would then be sent to your view by default, since no input connection capable of more sophisticated processing would have been defined.

But unfortunately, there does not seem to be a simple way to configure the EditorInfo attributes without overriding these methods and providing a BaseInputConnection object, and once you have done that you will have to dumb down the processing performed by that object as described above if you want to once again receive the raw key events.

WARNING: Two bugs were introduced in certain recent versions of the default LatinIME keyboard that ships with Android (Google Keyboard) that can impact keyboard event processing (as described above) when that keyboard is in use. I've devised some workarounds on the app side, with sample code, that appear to get around these problems. To view these workarounds, see the following answer:

https://stackoverflow.com/questions/18581636/android-cannot-capture-backspace-delete-press-in-soft-keyboard/19980975#19980975

Solution 2 - Android

According to the documentation, a View (editor) receives commands from the Keyboard (IME) through an InputConnection and sends commands to the Keyboard through an InputMethodManager.

enter image description here

I will show the entire code below, but here are the steps.

1. Make the Keyboard to appear

Since the view is sending a command to the keyboard it needs to use an InputMethodManager. For the sake of the example, we will say that when the view is tapped it will show the keyboard (or hide it if it is already showing).

@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;
}

The view also needs to have had setFocusableInTouchMode(true) set previously.

2. Receive input from the keyboard

In order for the view to receive input from the keyboard, it needs to override onCreateInputConnection(). This returns the InputConnection that the Keyboard uses to communicate with the view.

@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
    outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
    return new MyInputConnection(this, true);
}

The outAttrs specify what kind of keyboard the view is requesting. Here we are just requesting a normal text keyboard. Choosing TYPE_CLASS_NUMBER would display a number pad (if available). There are lots of other options. See EditorInfo.

You must return an InputConnection, which is usually a custom subclass of BaseInputConnection. In that subclass you provide a reference to your editable string, which the keyboard will make updates to. Since a SpannableStringBuilder implements the Editable interface, we will use that in our basic example.

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;
    }
}

All we did here was provide the input connection with a reference to the text variable in our custom view. The BaseInputConnection will take care of editing that mText. This could very well be all that you need to do. However, you can check out the source code and see which methods say "default implementation", especially "default implementation does nothing." These are other methods you may want to override depending on how involved your editor view is going to be. You should also look through all the method names in the documentation. A number of them have notes to "editor authors". Pay special attention to those.

Some keyboards don't send certain input through the InputConnection for some reason (for example delete, enter, and some number pad keys). For those I added an OnKeyListener. Testing this setup on five different soft keyboards, everything seemed to work. Supplemental answers related to this are here:

Full project code

Here is my full example for reference.

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>

There is nothing special in the MainActivity.java code.

Please leave a comment if this doesn't work for you. I am using this basic solution for a custom EditText in a library I am making and if there are any edge cases in which it doesn't work, I want to know. If you would like to view that project, the custom view is here. It's InputConnection is here.

Solution 3 - Android

Turns out that I did in fact need to subclass TextView and the use addTextChangedListener() to add my own implementation of TextWatcher in order to listen to soft key events. I couldn't find a way to do this with a plain old View.

One other thing, for those who will try this technique; TextView is not able to edit text by default, so if you want to make your implementation editable (instead of subclassing EditText, which I didn't want to to do), you must also make a custom InputConnection, something like the following:

 /**
 * 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;
	}
}

Then you override onCheckisTextEditor and onCreateInputConnection with something like the following:

 @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;
 }

After this, you should have a View that can listen to the soft keyboard and you can do whatever you want with the key input values.

Solution 4 - Android

My understanding is that your onKeyListener is only going to get hardware keyboard key events.

You will get access to all input key events if you override boolean View.onKeyPreIme(int keyCode, KeyEvent event)

This way you can elect to handle the key event action [ DOWN | MULTIPLE | UP ] and return true, or allow the normal key processing to deal with it (return super.onKeyPreIme())

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionrich.eView Question on Stackoverflow
Solution 1 - AndroidCarlView Answer on Stackoverflow
Solution 2 - AndroidSuragchView Answer on Stackoverflow
Solution 3 - Androidrich.eView Answer on Stackoverflow
Solution 4 - AndroidohhorobView Answer on Stackoverflow