Permalink
Browse files

Implement TextInput onContentSizeChange

Summary:
This adds proper support for tracking a TextInput content size as discussed in #6552 by adding a new callback that is called every time the content size changes including when first rendering the view.

Some points that are up for discussion are what do we want to do with the onChange callback as I don't see any use left for it now that we can track text change in onChangeText and size changes in onContentSizeChange. Also a bit off topic but should we consider renaming onChangeText to onTextChange to keep the naming more consistent (see [this naming justification](https://twitter.com/notbrent/status/709445076850597888)).

This is split in 2 commits for easier review, one for iOS and one for android.

The iOS implementation simply checks if the content size has changed everytime we update it and fire the callback, the only small issue was that the content size had several different values on initial render so I added a check to not fire events before the layoutSubviews where at this point the value is g
Closes #8457

Differential Revision: D3528202

Pulled By: dmmiller

fbshipit-source-id: fefe83f10cc5bfde1f5937c48c88b10408e58d9d
  • Loading branch information...
1 parent be0abd1 commit 2537157d994ac83f53a5ebc6ba5d74a9ad41f0c2 @janicduplessis janicduplessis committed with Facebook Github Bot 1 Jul 7, 2016
@@ -76,18 +76,21 @@ var TextEventsExample = React.createClass({
class AutoExpandingTextInput extends React.Component {
constructor(props) {
super(props);
- this.state = {text: '', height: 0};
+ this.state = {
+ text: 'React Native enables you to build world-class application experiences on native platforms using a consistent developer experience based on JavaScript and React. The focus of React Native is on developer efficiency across all the platforms you care about — learn once, write anywhere. Facebook uses React Native in multiple production apps and will continue investing in React Native.',
+ height: 0,
+ };
}
render() {
return (
<TextInput
{...this.props}
multiline={true}
- onChange={(event) => {
- this.setState({
- text: event.nativeEvent.text,
- height: event.nativeEvent.contentSize.height,
- });
+ onContentSizeChange={(event) => {
+ this.setState({height: event.nativeEvent.contentSize.height});
+ }}
+ onChangeText={(text) => {
+ this.setState({text});
}}
style={[styles.default, {height: Math.max(35, this.state.height)}]}
value={this.state.text}
@@ -102,18 +102,21 @@ class AutoExpandingTextInput extends React.Component {
constructor(props) {
super(props);
- this.state = {text: '', height: 0};
+ this.state = {
+ text: 'React Native enables you to build world-class application experiences on native platforms using a consistent developer experience based on JavaScript and React. The focus of React Native is on developer efficiency across all the platforms you care about — learn once, write anywhere. Facebook uses React Native in multiple production apps and will continue investing in React Native.',
+ height: 0,
+ };
}
render() {
return (
<TextInput
{...this.props}
multiline={true}
- onChange={(event) => {
- this.setState({
- text: event.nativeEvent.text,
- height: event.nativeEvent.contentSize.height,
- });
+ onChangeText={(text) => {
+ this.setState({text});
+ }}
+ onContentSizeChange={(event) => {
+ this.setState({height: event.nativeEvent.contentSize.height});
}}
style={[styles.default, {height: Math.max(35, this.state.height)}]}
value={this.state.text}
@@ -271,7 +271,7 @@ const TextInput = React.createClass({
* Sets the return key to the label. Use it instead of `returnKeyType`.
* @platform android
*/
- returnKeyLabel: PropTypes.string,
+ returnKeyLabel: PropTypes.string,
/**
* Limits the maximum number of characters that can be entered. Use this
* instead of implementing the logic in JS to avoid flicker.
@@ -312,6 +312,14 @@ const TextInput = React.createClass({
*/
onChangeText: PropTypes.func,
/**
+ * Callback that is called when the text input's content size changes.
+ * This will be called with
+ * `{ nativeEvent: { contentSize: { width, height } } }`.
+ *
+ * Only called for multiline text inputs.
+ */
+ onContentSizeChange: PropTypes.func,
+ /**
* Callback that is called when text input ends.
*/
onEndEditing: PropTypes.func,
@@ -581,6 +589,7 @@ const TextInput = React.createClass({
onFocus={this._onFocus}
onBlur={this._onBlur}
onChange={this._onChange}
+ onContentSizeChange={this.props.onContentSizeChange}
onSelectionChange={onSelectionChange}
onTextInput={this._onTextInput}
onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue}
@@ -641,6 +650,7 @@ const TextInput = React.createClass({
onFocus={this._onFocus}
onBlur={this._onBlur}
onChange={this._onChange}
+ onContentSizeChange={this.props.onContentSizeChange}
onSelectionChange={onSelectionChange}
onTextInput={this._onTextInput}
onEndEditing={this.props.onEndEditing}
@@ -29,6 +29,7 @@
@property (nonatomic, strong) NSNumber *maxLength;
@property (nonatomic, copy) RCTDirectEventBlock onChange;
+@property (nonatomic, copy) RCTDirectEventBlock onContentSizeChange;
@property (nonatomic, copy) RCTDirectEventBlock onSelectionChange;
@property (nonatomic, copy) RCTDirectEventBlock onTextInput;
@@ -77,6 +77,9 @@ @implementation RCTTextView
BOOL _blockTextShouldChange;
BOOL _nativeUpdatesInFlight;
NSInteger _nativeEventCount;
+
+ CGSize _previousContentSize;
+ BOOL _viewDidCompleteInitialLayout;
}
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
@@ -261,6 +264,17 @@ - (void)updateContentSize
size.height = [_textView sizeThatFits:size].height;
_scrollView.contentSize = size;
_textView.frame = (CGRect){CGPointZero, size};
+
+ if (_viewDidCompleteInitialLayout && _onContentSizeChange && !CGSizeEqualToSize(_previousContentSize, size)) {
+ _previousContentSize = size;
+ _onContentSizeChange(@{
+ @"contentSize": @{
+ @"height": @(size.height),
+ @"width": @(size.width),
+ },
+ @"target": self.reactTag,
+ });
+ }
}
- (void)updatePlaceholder
@@ -633,6 +647,11 @@ - (BOOL)resignFirstResponder
- (void)layoutSubviews
{
[super layoutSubviews];
+
+ // Start sending content size updates only after the view has been laid out
+ // otherwise we send multiple events with bad dimensions on initial render.
+ _viewDidCompleteInitialLayout = YES;
+
[self updateFrames];
}
@@ -35,6 +35,7 @@ - (UIView *)view
RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, textView.keyboardAppearance, UIKeyboardAppearance)
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(onContentSizeChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
@@ -71,11 +71,12 @@
/* package */ static Map getDirectEventTypeConstants() {
return MapBuilder.builder()
- .put("topSelectionChange", MapBuilder.of("registrationName", "onSelectionChange"))
- .put("topLoadingStart", MapBuilder.of("registrationName", "onLoadingStart"))
- .put("topLoadingFinish", MapBuilder.of("registrationName", "onLoadingFinish"))
- .put("topLoadingError", MapBuilder.of("registrationName", "onLoadingError"))
+ .put("topContentSizeChange", MapBuilder.of("registrationName", "onContentSizeChange"))
.put("topLayout", MapBuilder.of("registrationName", "onLayout"))
+ .put("topLoadingError", MapBuilder.of("registrationName", "onLoadingError"))
+ .put("topLoadingFinish", MapBuilder.of("registrationName", "onLoadingFinish"))
+ .put("topLoadingStart", MapBuilder.of("registrationName", "onLoadingStart"))
+ .put("topSelectionChange", MapBuilder.of("registrationName", "onSelectionChange"))
.build();
}
@@ -85,9 +85,6 @@ public void scrollTo(
Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.builder()
.put(ScrollEventType.SCROLL.getJSEventName(), MapBuilder.of("registrationName", "onScroll"))
- .put(
- ContentSizeChangeEvent.EVENT_NAME,
- MapBuilder.of("registrationName", "onContentSizeChange"))
.build();
}
}
@@ -0,0 +1,14 @@
+/**
+ * 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;
+
+public interface ContentSizeWatcher {
+ public void onLayout();
+}
@@ -0,0 +1,58 @@
+/**
+ * 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 content size changes.
+ */
+public class ReactContentSizeChangedEvent extends Event<ReactTextChangedEvent> {
+
+ public static final String EVENT_NAME = "topContentSizeChange";
+
+ private int mContentWidth;
+ private int mContentHeight;
+
+ public ReactContentSizeChangedEvent(
+ int viewId,
+ long timestampMs,
+ int contentSizeWidth,
+ int contentSizeHeight) {
+ super(viewId, timestampMs);
+ mContentWidth = contentSizeWidth;
+ mContentHeight = contentSizeHeight;
+ }
+
+ @Override
+ public String getEventName() {
+ return EVENT_NAME;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
+ }
+
+ private WritableMap serializeEventData() {
+ WritableMap eventData = Arguments.createMap();
+
+ WritableMap contentSize = Arguments.createMap();
+ contentSize.putDouble("width", mContentWidth);
+ contentSize.putDouble("height", mContentHeight);
+ eventData.putMap("contentSize", contentSize);
+
+ eventData.putInt("target", getViewTag());
+ return eventData;
+ }
+}
@@ -71,6 +71,7 @@
private boolean mContainsImages;
private boolean mBlurOnSubmit;
private @Nullable SelectionWatcher mSelectionWatcher;
+ private @Nullable ContentSizeWatcher mContentSizeWatcher;
private final InternalKeyListener mKeyListener;
private static final KeyListener sKeyListener = QwertyKeyListener.getInstanceForFullKeyboard();
@@ -102,15 +103,30 @@ public ReactEditText(Context context) {
// TODO: t6408636 verify if we should schedule a layout after a View does a requestLayout()
@Override
public boolean isLayoutRequested() {
- return false;
+ // If we are watching and updating container height based on content size
+ // then we don't want to scroll right away. This isn't perfect -- you might
+ // want to limit the height the text input can grow to. Possible solution
+ // is to add another prop that determines whether we should scroll to end
+ // of text.
+ if (mContentSizeWatcher != null) {
+ return isMultiline();
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (mContentSizeWatcher != null) {
+ mContentSizeWatcher.onLayout();
+ }
}
// Consume 'Enter' key events: TextView tries to give focus to the next TextInput, but it can't
// since we only allow JS to change focus, which in turn causes TextView to crash.
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_ENTER &&
- ((getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0 )) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER && !isMultiline()) {
hideSoftKeyboard();
return true;
}
@@ -162,6 +178,10 @@ public void removeTextChangedListener(TextWatcher watcher) {
}
}
+ public void setContentSizeWatcher(ContentSizeWatcher contentSizeWatcher) {
+ mContentSizeWatcher = contentSizeWatcher;
+ }
+
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
@@ -212,7 +232,7 @@ public void setInputType(int type) {
mStagedInputType = type;
// Input type password defaults to monospace font, so we need to re-apply the font
super.setTypeface(tf);
-
+
// We override the KeyListener so that all keys on the soft input keyboard as well as hardware
// keyboards work. Some KeyListeners like DigitsKeyListener will display the keyboard but not
// accept all input from it
@@ -329,6 +349,10 @@ private TextWatcherDelegator getTextWatcherDelegator() {
return mTextWatcherDelegator;
}
+ private boolean isMultiline() {
+ return (getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
+ }
+
/* package */ void setGravityHorizontal(int gravityHorizontal) {
if (gravityHorizontal == 0) {
gravityHorizontal = mDefaultGravityHorizontal;
@@ -447,7 +471,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
@Override
public void afterTextChanged(Editable s) {
if (!mIsSettingTextFromJS && mListeners != null) {
- for (android.text.TextWatcher listener : mListeners) {
+ for (TextWatcher listener : mListeners) {
listener.afterTextChanged(s);
}
}
Oops, something went wrong.

0 comments on commit 2537157

Please sign in to comment.