Implement onKeyPress Android
Summary: This implements onKeyPress for Android on TextInputs and addresses https://github.com/facebook/react-native/issues/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 https://github.com/facebook/react-native/pull/14720 Differential Revision: D6661592 Pulled By: hramos fbshipit-source-id: 5d53772dc2d127b002ea5fb84fa992934eb65a42
This commit is contained in:
parent
ddd65f1ba9
commit
c9ff0bc212
|
@ -422,7 +422,6 @@ const TextInput = createReactClass({
|
|||
* where `keyValue` is `'Enter'` or `'Backspace'` for respective keys and
|
||||
* the typed-in character otherwise including `' '` for space.
|
||||
* Fires before `onChange` callbacks.
|
||||
* @platform ios
|
||||
*/
|
||||
onKeyPress: PropTypes.func,
|
||||
/**
|
||||
|
|
|
@ -27,6 +27,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
|
|||
curText: '<No Event>',
|
||||
prevText: '<No Event>',
|
||||
prev2Text: '<No Event>',
|
||||
prev3Text: '<No Event>',
|
||||
};
|
||||
|
||||
updateText = (text) => {
|
||||
|
@ -35,6 +36,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
|
|||
curText: text,
|
||||
prevText: state.curText,
|
||||
prev2Text: state.prevText,
|
||||
prev3Text: state.prev2Text,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -46,6 +48,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
|
|||
autoCapitalize="none"
|
||||
placeholder="Enter text to see events"
|
||||
autoCorrect={false}
|
||||
multiline
|
||||
onFocus={() => this.updateText('onFocus')}
|
||||
onBlur={() => this.updateText('onBlur')}
|
||||
onChange={(event) => this.updateText(
|
||||
|
@ -60,12 +63,16 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
|
|||
onSubmitEditing={(event) => this.updateText(
|
||||
'onSubmitEditing text: ' + event.nativeEvent.text
|
||||
)}
|
||||
onKeyPress={(event) => this.updateText(
|
||||
'onKeyPress key: ' + event.nativeEvent.key
|
||||
)}
|
||||
style={styles.singleLine}
|
||||
/>
|
||||
<Text style={styles.eventLabel}>
|
||||
{this.state.curText}{'\n'}
|
||||
(prev: {this.state.prevText}){'\n'}
|
||||
(prev2: {this.state.prev2Text})
|
||||
(prev2: {this.state.prev2Text}){'\n'}
|
||||
(prev3: {this.state.prev3Text})
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -173,12 +173,15 @@ public class ReactEditText extends EditText {
|
|||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
InputConnection connection = super.onCreateInputConnection(outAttrs);
|
||||
ReactContext reactContext = (ReactContext) getContext();
|
||||
ReactEditTextInputConnectionWrapper inputConnectionWrapper =
|
||||
new ReactEditTextInputConnectionWrapper(super.onCreateInputConnection(outAttrs), reactContext, this);
|
||||
|
||||
if (isMultiline() && getBlurOnSubmit()) {
|
||||
// Remove IME_FLAG_NO_ENTER_ACTION to keep the original IME_OPTION
|
||||
outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
||||
}
|
||||
return connection;
|
||||
return inputConnectionWrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -138,6 +138,11 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
|
|||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture")))
|
||||
.put(
|
||||
"topKeyPress",
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of("bubbled", "onKeyPress", "captured", "onKeyPressCapture")))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue