-
Notifications
You must be signed in to change notification settings - Fork 24.5k
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
Implement onKeyPress Android #14720
Changes from 2 commits
fd566f3
74ccc2e
71124c0
7ad03a2
3efb3de
d37a01a
17085da
0a1bcc4
a330280
a1bf94f
97cc740
7fd20d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
/** | ||
* 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.inputmethod.EditorInfo; | ||
import android.view.inputmethod.InputConnection; | ||
import android.view.inputmethod.InputConnectionWrapper; | ||
import android.widget.EditText; | ||
|
||
import com.facebook.react.bridge.ReactContext; | ||
import com.facebook.react.uimanager.UIManagerModule; | ||
import com.facebook.react.uimanager.events.EventDispatcher; | ||
|
||
/** | ||
* This class wraps the {@link InputConnection} as returned by | ||
* {@link EditText#onCreateInputConnection(EditorInfo)} of the underlying {@link ReactEditText} | ||
* of {@link ReactTextInputManager} | ||
* The job of this class is to determine the key pressed by the soft keyboard. | ||
* | ||
* Firstly, we can make some deductions about a soft keyPress based on changes to the position of the | ||
* input cursor before and after the edit/change. We know that if there was no text selection before | ||
* the edit, and the cursor moves backwards, then it must be a delete; equally if it moves forwards | ||
* by a character, then we deduce the key input is the character preceding the new cursor position. | ||
* We also know if there was no text selection before the edit and the cursor was at the beginning of the input before, | ||
* and still is after,then it must also be a delete, i.e. an 'empty delete' where no text actually is deleted. | ||
* N.B. we are making the assumption that {@link InputConnection#endBatchEdit()} will fire in this case. | ||
* | ||
* In cases where there was a text selection before the edit, if the start of the selection is the same | ||
* after the edit as it was before, then we know it is a delete, if it is not the same, i.e. | ||
* it has moved forward a character, then we take that character to be the key input, i.e. a user | ||
* has selected some text and pressed a character to replace the selected text. | ||
* | ||
* With {@link EditText}s, text can be in two different states in the input itself, 'committed' & | ||
* currently 'composing'. N.B there is no composing text state when auto-correct is disabled, text | ||
* will be committed straight away character by character. | ||
* When a user is composing a word we get a callback to {@link InputConnection#setComposingText(CharSequence, int)} | ||
* with the entire word being composed. For example, composing 'hello' would result callbacks with | ||
* 'h', 'he', 'hel' 'hell', 'hello'. Our above logic for deriving the keyPress based on cursor position | ||
* handles this case. However we need additional logic surrounding the case whereby text can be committed. | ||
* | ||
* It is up to the IME to decide when text changes state from 'composing' to 'committed', | ||
* however the stock Android keyboard, for example, changes text being composed to be committed | ||
* when a user selects an auto-correction from the bar above the keyboard or presses 'space' or 'enter | ||
* to complete the word or text. In this case, our above logic with cursor positions does not apply, | ||
* as our cursor could be anywhere within the word being composed when a correction is selected, | ||
* and clearly selecting a single character from this correction would be the wrong thing to do. | ||
* It's fairly arbitrary, but we can set our keyPress to be the correction itself as this is what | ||
* the iOS implementation of 'onKeyPress' does. | ||
* In the case where a user commits with a space or enter, the stock IME first commits the composing text, | ||
* and then commits the user input of space or return afterwards, as a secondary commit within the batch edit. We of course | ||
* want the keyPress entered by the user, so we take the second of these two commits as the keyPress. | ||
* | ||
* A final case is the case whereby a user has committed some text, and their cursor comes straight | ||
* after the word they have just committed with no trailing space, as is the default behavior. If a user | ||
* is to input a character as to start a new word, the stock IME will first commit a space to the | ||
* input, and then set the composing text to be the character the user entered. In this case we | ||
* choose our onKeyPress to be the new composing character. | ||
*/ | ||
class ReactTextInputInputConnection extends InputConnectionWrapper { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. space character before |
||
public static final String NewLineRawValue = "\n"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. public static final fields should be all caps with underscores: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! |
||
public static final String BackspaceKeyValue = "Backspace"; | ||
public static final String EnterKeyValue = "Enter"; | ||
|
||
private @Nullable ReactEditText mEditText; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, with your suggested changes, |
||
private EventDispatcher mEventDispatcher; | ||
private int mPreviousSelectionStart; | ||
private int mPreviousSelectionEnd; | ||
private String mCommittedText; | ||
private String mComposedText; | ||
|
||
public ReactTextInputInputConnection( | ||
InputConnection target, | ||
boolean mutable, | ||
final ReactContext reactContext) { | ||
super(target, mutable); | ||
mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); | ||
} | ||
|
||
public void setEditText(final ReactEditText editText) { | ||
mEditText = editText; | ||
} | ||
|
||
@Override | ||
public boolean setComposingText(CharSequence text, int newCursorPosition) { | ||
mComposedText = text.toString(); | ||
return super.setComposingText(text, newCursorPosition); | ||
} | ||
|
||
@Override | ||
public boolean commitText(CharSequence text, int newCursorPosition) { | ||
mCommittedText = text.toString(); | ||
return super.commitText(text, newCursorPosition); | ||
} | ||
|
||
@Override | ||
public boolean beginBatchEdit() { | ||
mPreviousSelectionStart = mEditText.getSelectionStart(); | ||
mPreviousSelectionEnd = mEditText.getSelectionEnd(); | ||
mCommittedText = null; | ||
mComposedText = null; | ||
return super.beginBatchEdit(); | ||
} | ||
|
||
@Override | ||
public boolean endBatchEdit() { | ||
String key; | ||
int selectionStart = mEditText.getSelectionStart(); | ||
if (mCommittedText == null) { | ||
if ((noPreviousSelection() && selectionStart < mPreviousSelectionStart) | ||
|| (!noPreviousSelection() && selectionStart == mPreviousSelectionStart) | ||
|| (mPreviousSelectionStart == 0 && selectionStart == 0)) { | ||
key = BackspaceKeyValue; | ||
} else { | ||
char enteredChar = mEditText.getText().charAt(selectionStart - 1); | ||
key = String.valueOf(enteredChar); | ||
} | ||
} | ||
else { | ||
key = mComposedText != null ? mComposedText : mCommittedText; | ||
} | ||
key = keyValueFromString(key); | ||
mEventDispatcher.dispatchEvent( | ||
new ReactTextInputKeyPressEvent( | ||
mEditText.getId(), | ||
key)); | ||
return super.endBatchEdit(); | ||
} | ||
|
||
private boolean noPreviousSelection() { | ||
return mPreviousSelectionStart == mPreviousSelectionEnd; | ||
} | ||
|
||
private String keyValueFromString(final String key) { | ||
String returnValue; | ||
switch (key) { | ||
case "": returnValue = BackspaceKeyValue; | ||
break; | ||
case NewLineRawValue: returnValue = EnterKeyValue; | ||
break; | ||
default: | ||
returnValue = key; | ||
break; | ||
} | ||
return returnValue; | ||
} | ||
} |
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 |
---|---|---|
|
@@ -91,7 +91,9 @@ public String getName() { | |
|
||
@Override | ||
public ReactEditText createViewInstance(ThemedReactContext context) { | ||
ReactEditText editText = new ReactEditText(context); | ||
ReactTextInputInputConnection inputConnection = new ReactTextInputInputConnection(null, true, context); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic of instantiating InputConnection seems overly complicated. If you don't need to keep a reference to inputConnection outside of a) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you, very good point! |
||
ReactEditText editText = new ReactEditText(context, inputConnection); | ||
inputConnection.setEditText(editText); | ||
int inputType = editText.getInputType(); | ||
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE)); | ||
editText.setReturnKeyType("done"); | ||
|
@@ -141,6 +143,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(); | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am totally n00b in Android, but maybe we should call it
inputConnectionWrapper
instead?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'totally n00b in Android': that makes two of us then ;). Thank-you yes, good point!