-
Notifications
You must be signed in to change notification settings - Fork 24.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Summary: This implements onKeyPress for Android on TextInputs and addresses #1882. **N.B. that this PR has not yet addressed hardware keyboard inputs**, but doing will be fairly trivial. The main challenge was doing this for soft keyboard inputs. I've tried to match the style as much as I could. Will happily make any suggested edits be they architectural or stylistic design (edit: and of course implementation), but hopefully this is a good first pass :). I think important to test this on the most popular keyboard types; maybe different languages too. I have not yet added tests to test implementation, but will be happy to do that also. - Build & run RNTester project for Android and open TextInput. - Enter keys into 'event handling' TextInput. - Verify that keys you enter appear in onKeyPress below the text input - Test with autocorrect off, on same input and validate that results are the same. Below is a gif of PR in action. ![onkeypressandroid](https://user-images.githubusercontent.com/1807207/27512892-3f95c098-5949-11e7-9364-3ce9437f7bb9.gif) Closes #14720 Differential Revision: D6661592 Pulled By: hramos fbshipit-source-id: 5d53772dc2d127b002ea5fb84fa992934eb65a42
- Loading branch information
1 parent
ddd65f1
commit c9ff0bc
Showing
6 changed files
with
234 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
163 changes: 163 additions & 0 deletions
163
...src/main/java/com/facebook/react/views/textinput/ReactEditTextInputConnectionWrapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
/** | ||
* Copyright (c) 2015-present, Facebook, Inc. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. An additional grant | ||
* of patent rights can be found in the PATENTS file in the same directory. | ||
*/ | ||
|
||
package com.facebook.react.views.textinput; | ||
|
||
import javax.annotation.Nullable; | ||
|
||
import android.view.KeyEvent; | ||
import android.view.inputmethod.EditorInfo; | ||
import android.view.inputmethod.InputConnection; | ||
import android.view.inputmethod.InputConnectionWrapper; | ||
import com.facebook.react.bridge.ReactContext; | ||
import com.facebook.react.uimanager.UIManagerModule; | ||
import com.facebook.react.uimanager.events.EventDispatcher; | ||
|
||
/** | ||
* A class to implement the TextInput 'onKeyPress' API on android for soft keyboards. | ||
* It is instantiated in {@link ReactEditText#onCreateInputConnection(EditorInfo)}. | ||
* | ||
* Android IMEs interface with EditText views through the {@link InputConnection} interface, | ||
* so any observable change in state of the EditText via the soft-keyboard, should be a side effect of | ||
* one or more of the methods in {@link InputConnectionWrapper}. | ||
* | ||
* {@link InputConnection#setComposingText(CharSequence, int)} is used to set the composing region | ||
* (the underlined text) in the {@link android.widget.EditText} view, i.e. when React Native's | ||
* TextInput has the property 'autoCorrect' set to true. When text is being composed in the composing | ||
* state within the EditText, each key press will result in a call to | ||
* {@link InputConnection#setComposingText(CharSequence, int)} with a CharSequence argument equal to | ||
* that of the entire composing region, rather than a single character diff. | ||
* We can reason about the keyPress based on the resultant cursor position changes of the EditText after | ||
* applying this change. For example if the cursor moved backwards by one character when composing, | ||
* it's likely it was a delete; if it moves forward by a character, likely to be a key press of that character. | ||
* | ||
* IMEs can also call {@link InputConnection#beginBatchEdit()} to signify a batch of operations. One | ||
* such example is committing a word currently in composing state with the press of the space key. | ||
* It is IME dependent but the stock Android keyboard behavior seems to be to commit the currently composing | ||
* text with {@link InputConnection#setComposingText(CharSequence, int)} and commits a space character | ||
* with a separate call to {@link InputConnection#setComposingText(CharSequence, int)}. | ||
* Here we chose to emit the last input of a batch edit as that tends to be the user input, but | ||
* it's completely arbitrary. | ||
* | ||
* Another function of this class is to detect backspaces when the cursor at the beginning of the | ||
* {@link android.widget.EditText}, i.e no text is deleted. | ||
* | ||
* N.B. this class is only applicable for soft keyboards behavior. For hardware keyboards | ||
* {@link android.view.View#onKeyDown(int, KeyEvent)} can be overridden to obtain the keycode of the | ||
* key pressed. | ||
*/ | ||
class ReactEditTextInputConnectionWrapper extends InputConnectionWrapper { | ||
public static final String NEWLINE_RAW_VALUE = "\n"; | ||
public static final String BACKSPACE_KEY_VALUE = "Backspace"; | ||
public static final String ENTER_KEY_VALUE = "Enter"; | ||
|
||
private ReactEditText mEditText; | ||
private EventDispatcher mEventDispatcher; | ||
private boolean mIsBatchEdit; | ||
private @Nullable String mKey = null; | ||
|
||
public ReactEditTextInputConnectionWrapper( | ||
InputConnection target, | ||
final ReactContext reactContext, | ||
final ReactEditText editText | ||
) { | ||
super(target, false); | ||
mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); | ||
mEditText = editText; | ||
} | ||
|
||
@Override | ||
public boolean beginBatchEdit() { | ||
mIsBatchEdit = true; | ||
return super.beginBatchEdit(); | ||
} | ||
|
||
@Override | ||
public boolean endBatchEdit() { | ||
mIsBatchEdit = false; | ||
if (mKey != null) { | ||
dispatchKeyEvent(mKey); | ||
mKey = null; | ||
} | ||
return super.endBatchEdit(); | ||
} | ||
|
||
@Override | ||
public boolean setComposingText(CharSequence text, int newCursorPosition) { | ||
int previousSelectionStart = mEditText.getSelectionStart(); | ||
int previousSelectionEnd = mEditText.getSelectionEnd(); | ||
String key; | ||
boolean consumed = super.setComposingText(text, newCursorPosition); | ||
boolean noPreviousSelection = previousSelectionStart == previousSelectionEnd; | ||
boolean cursorDidNotMove = mEditText.getSelectionStart() == previousSelectionStart; | ||
boolean cursorMovedBackwards = mEditText.getSelectionStart() < previousSelectionStart; | ||
if ((noPreviousSelection && cursorMovedBackwards) | ||
|| !noPreviousSelection && cursorDidNotMove) { | ||
key = BACKSPACE_KEY_VALUE; | ||
} else { | ||
key = String.valueOf(mEditText.getText().charAt(mEditText.getSelectionStart() - 1)); | ||
} | ||
dispatchKeyEventOrEnqueue(key); | ||
return consumed; | ||
} | ||
|
||
@Override | ||
public boolean commitText(CharSequence text, int newCursorPosition) { | ||
String key = text.toString(); | ||
// Assume not a keyPress if length > 1 | ||
if (key.length() <= 1) { | ||
if (key.equals("")) { | ||
key = BACKSPACE_KEY_VALUE; | ||
} | ||
dispatchKeyEventOrEnqueue(key); | ||
} | ||
|
||
return super.commitText(text, newCursorPosition); | ||
} | ||
|
||
@Override | ||
public boolean deleteSurroundingText(int beforeLength, int afterLength) { | ||
dispatchKeyEvent(BACKSPACE_KEY_VALUE); | ||
return super.deleteSurroundingText(beforeLength, afterLength); | ||
} | ||
|
||
// Called by SwiftKey when cursor at beginning of input when there is a delete | ||
// or when enter is pressed anywhere in the text. Whereas stock Android Keyboard calls | ||
// {@link InputConnection#deleteSurroundingText} & {@link InputConnection#commitText} | ||
// in each case, respectively. | ||
@Override | ||
public boolean sendKeyEvent(KeyEvent event) { | ||
if(event.getAction() == KeyEvent.ACTION_DOWN) { | ||
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { | ||
dispatchKeyEvent(BACKSPACE_KEY_VALUE); | ||
} else if(event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { | ||
dispatchKeyEvent(ENTER_KEY_VALUE); | ||
} | ||
} | ||
return super.sendKeyEvent(event); | ||
} | ||
|
||
private void dispatchKeyEventOrEnqueue(String key) { | ||
if (mIsBatchEdit) { | ||
mKey = key; | ||
} else { | ||
dispatchKeyEvent(key); | ||
} | ||
} | ||
|
||
private void dispatchKeyEvent(String key) { | ||
if (key.equals(NEWLINE_RAW_VALUE)) { | ||
key = ENTER_KEY_VALUE; | ||
} | ||
mEventDispatcher.dispatchEvent( | ||
new ReactTextInputKeyPressEvent( | ||
mEditText.getId(), | ||
key)); | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
...Android/src/main/java/com/facebook/react/views/textinput/ReactTextInputKeyPressEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/** | ||
* Copyright (c) 2015-present, Facebook, Inc. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. An additional grant | ||
* of patent rights can be found in the PATENTS file in the same directory. | ||
*/ | ||
|
||
package com.facebook.react.views.textinput; | ||
|
||
import com.facebook.react.bridge.Arguments; | ||
import com.facebook.react.bridge.WritableMap; | ||
import com.facebook.react.uimanager.events.Event; | ||
import com.facebook.react.uimanager.events.RCTEventEmitter; | ||
|
||
/** | ||
* Event emitted by EditText native view when key pressed | ||
*/ | ||
public class ReactTextInputKeyPressEvent extends Event<ReactTextInputEvent> { | ||
|
||
public static final String EVENT_NAME = "topKeyPress"; | ||
|
||
private String mKey; | ||
|
||
ReactTextInputKeyPressEvent(int viewId, final String key) { | ||
super(viewId); | ||
mKey = key; | ||
} | ||
|
||
@Override | ||
public String getEventName() { | ||
return EVENT_NAME; | ||
} | ||
|
||
@Override | ||
public boolean canCoalesce() { | ||
// We don't want to miss any textinput event, as event data is incremental. | ||
return false; | ||
} | ||
|
||
@Override | ||
public void dispatch(RCTEventEmitter rctEventEmitter) { | ||
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); | ||
} | ||
|
||
private WritableMap serializeEventData() { | ||
WritableMap eventData = Arguments.createMap(); | ||
eventData.putString("key", mKey); | ||
|
||
return eventData; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters