Add TextInput controlled selection prop on Android

Summary:
Android PR for TextInput selection, based on the iOS implementation in #8958.

** Test plan **
Tested using the text selection example in UIExplorer.
Closes https://github.com/facebook/react-native/pull/8962

Differential Revision: D3819285

Pulled By: andreicoman11

fbshipit-source-id: 9a2408af2a8b694258c88ab5c46322830c71452a
This commit is contained in:
Janic Duplessis 2016-09-05 07:04:26 -07:00 committed by Facebook Github Bot
parent 2ea65ec872
commit 3c1b69c1a9
6 changed files with 159 additions and 7 deletions

View File

@ -258,6 +258,93 @@ class ToggleDefaultPaddingExample extends React.Component {
}
}
type SelectionExampleState = {
selection: {
start: number;
end: number;
};
value: string;
};
class SelectionExample extends React.Component {
state: SelectionExampleState;
_textInput: any;
constructor(props) {
super(props);
this.state = {
selection: {start: 0, end: 0},
value: props.value
};
}
onSelectionChange({nativeEvent: {selection}}) {
this.setState({selection});
}
getRandomPosition() {
var length = this.state.value.length;
return Math.round(Math.random() * length);
}
select(start, end) {
this._textInput.focus();
this.setState({selection: {start, end}});
}
selectRandom() {
var positions = [this.getRandomPosition(), this.getRandomPosition()].sort();
this.select(...positions);
}
placeAt(position) {
this.select(position, position);
}
placeAtRandom() {
this.placeAt(this.getRandomPosition());
}
render() {
var length = this.state.value.length;
return (
<View>
<TextInput
multiline={this.props.multiline}
onChangeText={(value) => this.setState({value})}
onSelectionChange={this.onSelectionChange.bind(this)}
ref={textInput => (this._textInput = textInput)}
selection={this.state.selection}
style={this.props.style}
value={this.state.value}
/>
<View>
<Text>
selection = {JSON.stringify(this.state.selection)}
</Text>
<Text onPress={this.placeAt.bind(this, 0)}>
Place at Start (0, 0)
</Text>
<Text onPress={this.placeAt.bind(this, length)}>
Place at End ({length}, {length})
</Text>
<Text onPress={this.placeAtRandom.bind(this)}>
Place at Random
</Text>
<Text onPress={this.select.bind(this, 0, length)}>
Select All
</Text>
<Text onPress={this.selectRandom.bind(this)}>
Select Random
</Text>
</View>
</View>
);
}
}
var styles = StyleSheet.create({
multiline: {
height: 60,
@ -499,19 +586,19 @@ exports.examples = [
placeholder="multiline, aligned top-left"
placeholderTextColor="red"
multiline={true}
style={[styles.multiline, {textAlign: "left", textAlignVertical: "top"}]}
style={[styles.multiline, {textAlign: 'left', textAlignVertical: 'top'}]}
/>
<TextInput
autoCorrect={true}
placeholder="multiline, aligned center"
placeholderTextColor="green"
multiline={true}
style={[styles.multiline, {textAlign: "center", textAlignVertical: "center"}]}
style={[styles.multiline, {textAlign: 'center', textAlignVertical: 'center'}]}
/>
<TextInput
autoCorrect={true}
multiline={true}
style={[styles.multiline, {color: 'blue'}, {textAlign: "right", textAlignVertical: "bottom"}]}>
style={[styles.multiline, {color: 'blue'}, {textAlign: 'right', textAlignVertical: 'bottom'}]}>
<Text style={styles.multiline}>multiline with children, aligned bottom-right</Text>
</TextInput>
</View>
@ -623,4 +710,22 @@ exports.examples = [
title: 'Toggle Default Padding',
render: function(): ReactElement { return <ToggleDefaultPaddingExample />; },
},
{
title: 'Text selection & cursor placement',
render: function() {
return (
<View>
<SelectionExample
style={styles.default}
value="text selection can be changed"
/>
<SelectionExample
multiline
style={styles.multiline}
value={"multiline text selection\ncan also be changed"}
/>
</View>
);
}
},
];

View File

@ -393,7 +393,6 @@ const TextInput = React.createClass({
/**
* The start and end of the text input's selection. Set start and end to
* the same value to position the cursor.
* @platform ios
*/
selection: PropTypes.shape({
start: PropTypes.number.isRequired,
@ -679,6 +678,10 @@ const TextInput = React.createClass({
children = <Text>{children}</Text>;
}
if (props.selection && props.selection.end == null) {
props.selection = {start: props.selection.start, end: props.selection.start};
}
const textContainer =
<AndroidTextInput
ref={this._setNativeRef}

View File

@ -64,6 +64,7 @@ public class ReactEditText extends EditText {
private int mDefaultGravityHorizontal;
private int mDefaultGravityVertical;
private int mNativeEventCount;
private int mMostRecentEventCount;
private @Nullable ArrayList<TextWatcher> mListeners;
private @Nullable TextWatcherDelegator mTextWatcherDelegator;
private int mStagedInputType;
@ -86,6 +87,7 @@ public class ReactEditText extends EditText {
getGravity() & (Gravity.HORIZONTAL_GRAVITY_MASK | Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK);
mDefaultGravityVertical = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
mNativeEventCount = 0;
mMostRecentEventCount = 0;
mIsSettingTextFromJS = false;
mIsJSSettingFocus = false;
mBlurOnSubmit = true;
@ -182,6 +184,16 @@ public class ReactEditText extends EditText {
mContentSizeWatcher = contentSizeWatcher;
}
@Override
public void setSelection(int start, int end) {
// Skip setting the selection if the text wasn't set because of an out of date value.
if (mMostRecentEventCount < mNativeEventCount) {
return;
}
super.setSelection(start, end);
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
@ -265,7 +277,8 @@ public class ReactEditText extends EditText {
// VisibleForTesting from {@link TextInputEventsTestCase}.
public void maybeSetText(ReactTextUpdate reactTextUpdate) {
// Only set the text if it is up to date.
if (reactTextUpdate.getJsEventCounter() < mNativeEventCount) {
mMostRecentEventCount = reactTextUpdate.getJsEventCounter();
if (mMostRecentEventCount < mNativeEventCount) {
return;
}

View File

@ -32,6 +32,7 @@ import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.BaseViewManager;
import com.facebook.react.uimanager.LayoutShadowNode;
@ -230,6 +231,17 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
}
}
@ReactProp(name = "selection")
public void setSelection(ReactEditText view, @Nullable ReadableMap selection) {
if (selection == null) {
return;
}
if (selection.hasKey("start") && selection.hasKey("end")) {
view.setSelection(selection.getInt("start"), selection.getInt("end"));
}
}
@ReactProp(name = "onSelectionChange", defaultBoolean = false)
public void setOnSelectionChange(final ReactEditText view, boolean onSelectionChange) {
if (onSelectionChange) {

View File

@ -48,8 +48,8 @@ import com.facebook.react.uimanager.events.RCTEventEmitter;
WritableMap eventData = Arguments.createMap();
WritableMap selectionData = Arguments.createMap();
selectionData.putInt("start", mSelectionStart);
selectionData.putInt("end", mSelectionEnd);
selectionData.putInt("start", mSelectionStart);
eventData.putMap("selection", selectionData);
return eventData;

View File

@ -340,4 +340,23 @@ public class ReactTextInputPropertyTest {
mManager.setMaxLength(view, null);
assertThat(view.getFilters()).isEqualTo(filters);
}
@Test
public void testSelection() {
ReactEditText view = mManager.createViewInstance(mThemedContext);
view.setText("Need some text to select something...");
mManager.updateProperties(view, buildStyles());
assertThat(view.getSelectionStart()).isEqualTo(0);
assertThat(view.getSelectionEnd()).isEqualTo(0);
JavaOnlyMap selection = JavaOnlyMap.of("start", 5, "end", 10);
mManager.updateProperties(view, buildStyles("selection", selection));
assertThat(view.getSelectionStart()).isEqualTo(5);
assertThat(view.getSelectionEnd()).isEqualTo(10);
mManager.updateProperties(view, buildStyles("selection", null));
assertThat(view.getSelectionStart()).isEqualTo(5);
assertThat(view.getSelectionEnd()).isEqualTo(10);
}
}