Skip to content

Commit c9ff0bc

Browse files
Implement onKeyPress Android
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
1 parent ddd65f1 commit c9ff0bc

File tree

6 files changed

+234
-4
lines changed

6 files changed

+234
-4
lines changed

Libraries/Components/TextInput/TextInput.js

-1
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,6 @@ const TextInput = createReactClass({
422422
* where `keyValue` is `'Enter'` or `'Backspace'` for respective keys and
423423
* the typed-in character otherwise including `' '` for space.
424424
* Fires before `onChange` callbacks.
425-
* @platform ios
426425
*/
427426
onKeyPress: PropTypes.func,
428427
/**

RNTester/js/TextInputExample.android.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
2727
curText: '<No Event>',
2828
prevText: '<No Event>',
2929
prev2Text: '<No Event>',
30+
prev3Text: '<No Event>',
3031
};
3132

3233
updateText = (text) => {
@@ -35,6 +36,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
3536
curText: text,
3637
prevText: state.curText,
3738
prev2Text: state.prevText,
39+
prev3Text: state.prev2Text,
3840
};
3941
});
4042
};
@@ -46,6 +48,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
4648
autoCapitalize="none"
4749
placeholder="Enter text to see events"
4850
autoCorrect={false}
51+
multiline
4952
onFocus={() => this.updateText('onFocus')}
5053
onBlur={() => this.updateText('onBlur')}
5154
onChange={(event) => this.updateText(
@@ -60,12 +63,16 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
6063
onSubmitEditing={(event) => this.updateText(
6164
'onSubmitEditing text: ' + event.nativeEvent.text
6265
)}
66+
onKeyPress={(event) => this.updateText(
67+
'onKeyPress key: ' + event.nativeEvent.key
68+
)}
6369
style={styles.singleLine}
6470
/>
6571
<Text style={styles.eventLabel}>
6672
{this.state.curText}{'\n'}
6773
(prev: {this.state.prevText}){'\n'}
68-
(prev2: {this.state.prev2Text})
74+
(prev2: {this.state.prev2Text}){'\n'}
75+
(prev3: {this.state.prev3Text})
6976
</Text>
7077
</View>
7178
);

ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,15 @@ protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
173173

174174
@Override
175175
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
176-
InputConnection connection = super.onCreateInputConnection(outAttrs);
176+
ReactContext reactContext = (ReactContext) getContext();
177+
ReactEditTextInputConnectionWrapper inputConnectionWrapper =
178+
new ReactEditTextInputConnectionWrapper(super.onCreateInputConnection(outAttrs), reactContext, this);
179+
177180
if (isMultiline() && getBlurOnSubmit()) {
178181
// Remove IME_FLAG_NO_ENTER_ACTION to keep the original IME_OPTION
179182
outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
180183
}
181-
return connection;
184+
return inputConnectionWrapper;
182185
}
183186

184187
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
package com.facebook.react.views.textinput;
11+
12+
import javax.annotation.Nullable;
13+
14+
import android.view.KeyEvent;
15+
import android.view.inputmethod.EditorInfo;
16+
import android.view.inputmethod.InputConnection;
17+
import android.view.inputmethod.InputConnectionWrapper;
18+
import com.facebook.react.bridge.ReactContext;
19+
import com.facebook.react.uimanager.UIManagerModule;
20+
import com.facebook.react.uimanager.events.EventDispatcher;
21+
22+
/**
23+
* A class to implement the TextInput 'onKeyPress' API on android for soft keyboards.
24+
* It is instantiated in {@link ReactEditText#onCreateInputConnection(EditorInfo)}.
25+
*
26+
* Android IMEs interface with EditText views through the {@link InputConnection} interface,
27+
* so any observable change in state of the EditText via the soft-keyboard, should be a side effect of
28+
* one or more of the methods in {@link InputConnectionWrapper}.
29+
*
30+
* {@link InputConnection#setComposingText(CharSequence, int)} is used to set the composing region
31+
* (the underlined text) in the {@link android.widget.EditText} view, i.e. when React Native's
32+
* TextInput has the property 'autoCorrect' set to true. When text is being composed in the composing
33+
* state within the EditText, each key press will result in a call to
34+
* {@link InputConnection#setComposingText(CharSequence, int)} with a CharSequence argument equal to
35+
* that of the entire composing region, rather than a single character diff.
36+
* We can reason about the keyPress based on the resultant cursor position changes of the EditText after
37+
* applying this change. For example if the cursor moved backwards by one character when composing,
38+
* it's likely it was a delete; if it moves forward by a character, likely to be a key press of that character.
39+
*
40+
* IMEs can also call {@link InputConnection#beginBatchEdit()} to signify a batch of operations. One
41+
* such example is committing a word currently in composing state with the press of the space key.
42+
* It is IME dependent but the stock Android keyboard behavior seems to be to commit the currently composing
43+
* text with {@link InputConnection#setComposingText(CharSequence, int)} and commits a space character
44+
* with a separate call to {@link InputConnection#setComposingText(CharSequence, int)}.
45+
* Here we chose to emit the last input of a batch edit as that tends to be the user input, but
46+
* it's completely arbitrary.
47+
*
48+
* Another function of this class is to detect backspaces when the cursor at the beginning of the
49+
* {@link android.widget.EditText}, i.e no text is deleted.
50+
*
51+
* N.B. this class is only applicable for soft keyboards behavior. For hardware keyboards
52+
* {@link android.view.View#onKeyDown(int, KeyEvent)} can be overridden to obtain the keycode of the
53+
* key pressed.
54+
*/
55+
class ReactEditTextInputConnectionWrapper extends InputConnectionWrapper {
56+
public static final String NEWLINE_RAW_VALUE = "\n";
57+
public static final String BACKSPACE_KEY_VALUE = "Backspace";
58+
public static final String ENTER_KEY_VALUE = "Enter";
59+
60+
private ReactEditText mEditText;
61+
private EventDispatcher mEventDispatcher;
62+
private boolean mIsBatchEdit;
63+
private @Nullable String mKey = null;
64+
65+
public ReactEditTextInputConnectionWrapper(
66+
InputConnection target,
67+
final ReactContext reactContext,
68+
final ReactEditText editText
69+
) {
70+
super(target, false);
71+
mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
72+
mEditText = editText;
73+
}
74+
75+
@Override
76+
public boolean beginBatchEdit() {
77+
mIsBatchEdit = true;
78+
return super.beginBatchEdit();
79+
}
80+
81+
@Override
82+
public boolean endBatchEdit() {
83+
mIsBatchEdit = false;
84+
if (mKey != null) {
85+
dispatchKeyEvent(mKey);
86+
mKey = null;
87+
}
88+
return super.endBatchEdit();
89+
}
90+
91+
@Override
92+
public boolean setComposingText(CharSequence text, int newCursorPosition) {
93+
int previousSelectionStart = mEditText.getSelectionStart();
94+
int previousSelectionEnd = mEditText.getSelectionEnd();
95+
String key;
96+
boolean consumed = super.setComposingText(text, newCursorPosition);
97+
boolean noPreviousSelection = previousSelectionStart == previousSelectionEnd;
98+
boolean cursorDidNotMove = mEditText.getSelectionStart() == previousSelectionStart;
99+
boolean cursorMovedBackwards = mEditText.getSelectionStart() < previousSelectionStart;
100+
if ((noPreviousSelection && cursorMovedBackwards)
101+
|| !noPreviousSelection && cursorDidNotMove) {
102+
key = BACKSPACE_KEY_VALUE;
103+
} else {
104+
key = String.valueOf(mEditText.getText().charAt(mEditText.getSelectionStart() - 1));
105+
}
106+
dispatchKeyEventOrEnqueue(key);
107+
return consumed;
108+
}
109+
110+
@Override
111+
public boolean commitText(CharSequence text, int newCursorPosition) {
112+
String key = text.toString();
113+
// Assume not a keyPress if length > 1
114+
if (key.length() <= 1) {
115+
if (key.equals("")) {
116+
key = BACKSPACE_KEY_VALUE;
117+
}
118+
dispatchKeyEventOrEnqueue(key);
119+
}
120+
121+
return super.commitText(text, newCursorPosition);
122+
}
123+
124+
@Override
125+
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
126+
dispatchKeyEvent(BACKSPACE_KEY_VALUE);
127+
return super.deleteSurroundingText(beforeLength, afterLength);
128+
}
129+
130+
// Called by SwiftKey when cursor at beginning of input when there is a delete
131+
// or when enter is pressed anywhere in the text. Whereas stock Android Keyboard calls
132+
// {@link InputConnection#deleteSurroundingText} & {@link InputConnection#commitText}
133+
// in each case, respectively.
134+
@Override
135+
public boolean sendKeyEvent(KeyEvent event) {
136+
if(event.getAction() == KeyEvent.ACTION_DOWN) {
137+
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
138+
dispatchKeyEvent(BACKSPACE_KEY_VALUE);
139+
} else if(event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
140+
dispatchKeyEvent(ENTER_KEY_VALUE);
141+
}
142+
}
143+
return super.sendKeyEvent(event);
144+
}
145+
146+
private void dispatchKeyEventOrEnqueue(String key) {
147+
if (mIsBatchEdit) {
148+
mKey = key;
149+
} else {
150+
dispatchKeyEvent(key);
151+
}
152+
}
153+
154+
private void dispatchKeyEvent(String key) {
155+
if (key.equals(NEWLINE_RAW_VALUE)) {
156+
key = ENTER_KEY_VALUE;
157+
}
158+
mEventDispatcher.dispatchEvent(
159+
new ReactTextInputKeyPressEvent(
160+
mEditText.getId(),
161+
key));
162+
}
163+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
package com.facebook.react.views.textinput;
11+
12+
import com.facebook.react.bridge.Arguments;
13+
import com.facebook.react.bridge.WritableMap;
14+
import com.facebook.react.uimanager.events.Event;
15+
import com.facebook.react.uimanager.events.RCTEventEmitter;
16+
17+
/**
18+
* Event emitted by EditText native view when key pressed
19+
*/
20+
public class ReactTextInputKeyPressEvent extends Event<ReactTextInputEvent> {
21+
22+
public static final String EVENT_NAME = "topKeyPress";
23+
24+
private String mKey;
25+
26+
ReactTextInputKeyPressEvent(int viewId, final String key) {
27+
super(viewId);
28+
mKey = key;
29+
}
30+
31+
@Override
32+
public String getEventName() {
33+
return EVENT_NAME;
34+
}
35+
36+
@Override
37+
public boolean canCoalesce() {
38+
// We don't want to miss any textinput event, as event data is incremental.
39+
return false;
40+
}
41+
42+
@Override
43+
public void dispatch(RCTEventEmitter rctEventEmitter) {
44+
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
45+
}
46+
47+
private WritableMap serializeEventData() {
48+
WritableMap eventData = Arguments.createMap();
49+
eventData.putString("key", mKey);
50+
51+
return eventData;
52+
}
53+
}

ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java

+5
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
138138
MapBuilder.of(
139139
"phasedRegistrationNames",
140140
MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture")))
141+
.put(
142+
"topKeyPress",
143+
MapBuilder.of(
144+
"phasedRegistrationNames",
145+
MapBuilder.of("bubbled", "onKeyPress", "captured", "onKeyPressCapture")))
141146
.build();
142147
}
143148

0 commit comments

Comments
 (0)