Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement onKeyPress Android #14720

1 change: 0 additions & 1 deletion Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
/**
Expand Down
9 changes: 8 additions & 1 deletion RNTester/js/TextInputExample.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
curText: '<No Event>',
prevText: '<No Event>',
prev2Text: '<No Event>',
prev3Text: '<No Event>',
};

updateText = (text) => {
Expand All @@ -35,6 +36,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
curText: text,
prevText: state.curText,
prev2Text: state.prevText,
prev3Text: state.prev2Text,
};
});
};
Expand All @@ -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(
Expand All @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,15 @@ protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {

@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
Expand Down
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));
}
}
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture")))
.put(
"topKeyPress",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onKeyPress", "captured", "onKeyPressCapture")))
.build();
}

Expand Down