-
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 10 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,164 @@ | ||
/** | ||
* 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, | ||
boolean mutable, | ||
final ReactContext reactContext, | ||
final ReactEditText editText | ||
) { | ||
super(target, mutable); | ||
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) { | ||
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: |
||
dispatchKeyEvent(mKey); | ||
mKey = null; | ||
} | ||
return super.endBatchEdit(); | ||
} | ||
|
||
@Override | ||
public boolean setComposingText(CharSequence text, int newCursorPosition) { | ||
final int previousSelectionStart = mEditText.getSelectionStart(); | ||
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. can you drop all the 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! |
||
final int previousSelectionEnd = mEditText.getSelectionEnd(); | ||
String key; | ||
final boolean consumed = super.setComposingText(text, newCursorPosition); | ||
final boolean noPreviousSelection = previousSelectionStart == previousSelectionEnd; | ||
final boolean cursorDidNotMove = mEditText.getSelectionStart() == previousSelectionStart; | ||
final 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; | ||
} | ||
} |
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.
As we are no longer updating
target
we can just default tomutable=false
. Also there seem to be no need to exposemutable
as an argument of your subclass constructor (ReactEditTextInputConnectionWrapper). I recommend to replace this call with:And remove
boolean mutable
from the list of constructor arguments.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.
Good spot, thanks.