Skip to content

Commit 3fad859

Browse files
joshjhargreavesjsm
authored andcommitted
Implement onKeyPress Android
Summary: This implements onKeyPress for Android on TextInputs and addresses facebook#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 facebook#14720 Differential Revision: D6661592 Pulled By: hramos fbshipit-source-id: 5d53772dc2d127b002ea5fb84fa992934eb65a42
1 parent afdd0d5 commit 3fad859

File tree

6 files changed

+234
-4
lines changed

6 files changed

+234
-4
lines changed

Libraries/Components/TextInput/TextInput.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,6 @@ const TextInput = createReactClass({
400400
* where `keyValue` is `'Enter'` or `'Backspace'` for respective keys and
401401
* the typed-in character otherwise including `' '` for space.
402402
* Fires before `onChange` callbacks.
403-
* @platform ios
404403
*/
405404
onKeyPress: PropTypes.func,
406405
/**

RNTester/js/TextInputExample.android.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
2525
curText: '<No Event>',
2626
prevText: '<No Event>',
2727
prev2Text: '<No Event>',
28+
prev3Text: '<No Event>',
2829
};
2930

3031
updateText = (text) => {
@@ -33,6 +34,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
3334
curText: text,
3435
prevText: state.curText,
3536
prev2Text: state.prevText,
37+
prev3Text: state.prev2Text,
3638
};
3739
});
3840
};
@@ -44,6 +46,7 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
4446
autoCapitalize="none"
4547
placeholder="Enter text to see events"
4648
autoCorrect={false}
49+
multiline
4750
onFocus={() => this.updateText('onFocus')}
4851
onBlur={() => this.updateText('onBlur')}
4952
onChange={(event) => this.updateText(
@@ -58,12 +61,16 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> {
5861
onSubmitEditing={(event) => this.updateText(
5962
'onSubmitEditing text: ' + event.nativeEvent.text
6063
)}
64+
onKeyPress={(event) => this.updateText(
65+
'onKeyPress key: ' + event.nativeEvent.key
66+
)}
6167
style={styles.singleLine}
6268
/>
6369
<Text style={styles.eventLabel}>
6470
{this.state.curText}{'\n'}
6571
(prev: {this.state.prevText}){'\n'}
66-
(prev2: {this.state.prev2Text})
72+
(prev2: {this.state.prev2Text}){'\n'}
73+
(prev3: {this.state.prev3Text})
6774
</Text>
6875
</View>
6976
);

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,15 @@ protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
185185

186186
@Override
187187
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
188-
InputConnection connection = super.onCreateInputConnection(outAttrs);
188+
ReactContext reactContext = (ReactContext) getContext();
189+
ReactEditTextInputConnectionWrapper inputConnectionWrapper =
190+
new ReactEditTextInputConnectionWrapper(super.onCreateInputConnection(outAttrs), reactContext, this);
191+
189192
if (isMultiline() && getBlurOnSubmit()) {
190193
// Remove IME_FLAG_NO_ENTER_ACTION to keep the original IME_OPTION
191194
outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
192195
}
193-
return connection;
196+
return inputConnectionWrapper;
194197
}
195198

196199
@Override
Lines changed: 163 additions & 0 deletions
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+
}
Lines changed: 53 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
141141
MapBuilder.of(
142142
"phasedRegistrationNames",
143143
MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture")))
144+
.put(
145+
"topKeyPress",
146+
MapBuilder.of(
147+
"phasedRegistrationNames",
148+
MapBuilder.of("bubbled", "onKeyPress", "captured", "onKeyPressCapture")))
144149
.build();
145150
}
146151

0 commit comments

Comments
 (0)