Fix flaky scrolling for TextInput when using rich text

Summary: public

This diff fixes the jumpy scrolling for multiline `<TextInput>` when using nested `<Text>` components to implement rich text highlighting.

The fix is to disable scrolling on the underlying UITextView, and nest it inside another UIScrollView that we control.

Reviewed By: ericvicenti, tadeuzagallo

Differential Revision: D2674670

fb-gh-sync-id: bacee3ae485523cc26ca8102b714e081df230629
This commit is contained in:
Nick Lockwood 2015-11-24 14:26:14 -08:00 committed by facebook-github-bot-7
parent 469a65217c
commit b5be05d82b
4 changed files with 74 additions and 75 deletions

View File

@ -301,6 +301,50 @@ exports.displayName = (undefined: ?string);
exports.title = '<TextInput>'; exports.title = '<TextInput>';
exports.description = 'Single and multi-line text inputs.'; exports.description = 'Single and multi-line text inputs.';
exports.examples = [ exports.examples = [
{
title: 'Multiline',
render: function() {
return (
<View>
<TextInput
placeholder="multiline text input"
multiline={true}
style={styles.multiline}
/>
<TextInput
placeholder="multiline text input with font styles and placeholder"
multiline={true}
clearTextOnFocus={true}
autoCorrect={true}
autoCapitalize="words"
placeholderTextColor="red"
keyboardType="url"
style={[styles.multiline, styles.multilineWithFontStyles]}
/>
<TextInput
placeholder="uneditable multiline text input"
editable={false}
multiline={true}
style={styles.multiline}
/>
<TextInput
placeholder="multiline with children"
multiline={true}
enablesReturnKeyAutomatically={true}
returnKeyType="go"
style={styles.multiline}>
<View style={styles.multilineChild}/>
</TextInput>
</View>
);
}
},
{
title: 'Attributed text',
render: function() {
return <TokenizedTextExample />;
}
},
{ {
title: 'Auto-focus', title: 'Auto-focus',
render: function() { render: function() {
@ -544,50 +588,6 @@ exports.examples = [
); );
} }
}, },
{
title: 'Multiline',
render: function() {
return (
<View>
<TextInput
placeholder="multiline text input"
multiline={true}
style={styles.multiline}
/>
<TextInput
placeholder="multiline text input with font styles and placeholder"
multiline={true}
clearTextOnFocus={true}
autoCorrect={true}
autoCapitalize="words"
placeholderTextColor="red"
keyboardType="url"
style={[styles.multiline, styles.multilineWithFontStyles]}
/>
<TextInput
placeholder="uneditable multiline text input"
editable={false}
multiline={true}
style={styles.multiline}
/>
<TextInput
placeholder="multiline with children"
multiline={true}
enablesReturnKeyAutomatically={true}
returnKeyType="go"
style={styles.multiline}>
<View style={styles.multilineChild}/>
</TextInput>
</View>
);
}
},
{
title: 'Attributed text',
render: function() {
return <TokenizedTextExample />;
}
},
{ {
title: 'Blur on submit', title: 'Blur on submit',
render: function(): ReactElement { return <BlurOnSubmitExample />; }, render: function(): ReactElement { return <BlurOnSubmitExample />; },

View File

@ -22,7 +22,6 @@
@property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
@property (nonatomic, copy) NSString *text; @property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIColor *placeholderTextColor; @property (nonatomic, strong) UIColor *placeholderTextColor;
@property (nonatomic, strong) UIFont *font; @property (nonatomic, strong) UIFont *font;
@property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, assign) NSInteger mostRecentEventCount;

View File

@ -44,6 +44,7 @@
NSMutableArray<UIView *> *_subviews; NSMutableArray<UIView *> *_subviews;
BOOL _blockTextShouldChange; BOOL _blockTextShouldChange;
UITextRange *_previousSelectionRange; UITextRange *_previousSelectionRange;
UIScrollView *_scrollView;
} }
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
@ -55,15 +56,19 @@
_eventDispatcher = eventDispatcher; _eventDispatcher = eventDispatcher;
_placeholderTextColor = [self defaultPlaceholderTextColor]; _placeholderTextColor = [self defaultPlaceholderTextColor];
_textView = [[RCTUITextView alloc] initWithFrame:self.bounds]; _textView = [[RCTUITextView alloc] initWithFrame:CGRectZero];
_textView.backgroundColor = [UIColor clearColor]; _textView.backgroundColor = [UIColor clearColor];
_textView.scrollsToTop = NO; _textView.scrollsToTop = NO;
_textView.scrollEnabled = NO;
_textView.delegate = self; _textView.delegate = self;
_scrollView = [[UIScrollView alloc] initWithFrame:CGRectZero];
[_scrollView addSubview:_textView];
_previousSelectionRange = _textView.selectedTextRange; _previousSelectionRange = _textView.selectedTextRange;
_subviews = [NSMutableArray new]; _subviews = [NSMutableArray new];
[self addSubview:_textView]; [self addSubview:_scrollView];
} }
return self; return self;
} }
@ -139,16 +144,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
// we temporarily block all textShouldChange events so they are not applied. // we temporarily block all textShouldChange events so they are not applied.
_blockTextShouldChange = YES; _blockTextShouldChange = YES;
// We compute the new selectedRange manually to make sure the cursor is at the
// end of the newly inserted/deleted text after update.
NSRange range = _textView.selectedRange; NSRange range = _textView.selectedRange;
CGPoint contentOffset = _textView.contentOffset;
_textView.attributedText = _pendingAttributedText; _textView.attributedText = _pendingAttributedText;
_pendingAttributedText = nil; _pendingAttributedText = nil;
_textView.selectedRange = range; _textView.selectedRange = range;
[_textView layoutIfNeeded]; [_textView layoutIfNeeded];
_textView.contentOffset = contentOffset;
[self _setPlaceholderVisibility]; [self _setPlaceholderVisibility];
@ -174,11 +174,21 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
CGRect frame = UIEdgeInsetsInsetRect(self.bounds, adjustedFrameInset); CGRect frame = UIEdgeInsetsInsetRect(self.bounds, adjustedFrameInset);
_textView.frame = frame; _textView.frame = frame;
_placeholderView.frame = frame; _placeholderView.frame = frame;
_scrollView.frame = frame;
[self updateContentSize];
_textView.textContainerInset = adjustedTextContainerInset; _textView.textContainerInset = adjustedTextContainerInset;
_placeholderView.textContainerInset = adjustedTextContainerInset; _placeholderView.textContainerInset = adjustedTextContainerInset;
} }
- (void)updateContentSize
{
_textView.scrollEnabled = YES;
_scrollView.contentSize = _textView.contentSize;
_textView.frame = (CGRect){CGPointZero, _scrollView.contentSize};
_textView.scrollEnabled = NO;
}
- (void)updatePlaceholder - (void)updatePlaceholder
{ {
[_placeholderView removeFromSuperview]; [_placeholderView removeFromSuperview];
@ -186,6 +196,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
if (_placeholder) { if (_placeholder) {
_placeholderView = [[UITextView alloc] initWithFrame:self.bounds]; _placeholderView = [[UITextView alloc] initWithFrame:self.bounds];
_placeholderView.editable = NO;
_placeholderView.userInteractionEnabled = NO;
_placeholderView.backgroundColor = [UIColor clearColor]; _placeholderView.backgroundColor = [UIColor clearColor];
_placeholderView.scrollEnabled = false; _placeholderView.scrollEnabled = false;
_placeholderView.scrollsToTop = NO; _placeholderView.scrollsToTop = NO;
@ -211,16 +223,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
[self updatePlaceholder]; [self updatePlaceholder];
} }
- (UIColor *)textColor
{
return _textView.textColor;
}
- (void)setTextColor:(UIColor *)textColor
{
_textView.textColor = textColor;
}
- (void)setPlaceholder:(NSString *)placeholder - (void)setPlaceholder:(NSString *)placeholder
{ {
_placeholder = placeholder; _placeholder = placeholder;
@ -305,10 +307,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
}, },
}); });
} }
if (textView.editable && [textView isFirstResponder]) {
[textView scrollRangeToVisible:textView.selectedRange];
}
} }
- (void)setText:(NSString *)text - (void)setText:(NSString *)text
@ -369,6 +367,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (void)textViewDidChange:(UITextView *)textView - (void)textViewDidChange:(UITextView *)textView
{ {
[self updateContentSize];
[self _setPlaceholderVisibility]; [self _setPlaceholderVisibility];
_nativeEventCount++; _nativeEventCount++;
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange

View File

@ -23,21 +23,22 @@ RCT_EXPORT_MODULE()
return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
} }
RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.autocapitalizationType, UITextAutocapitalizationType)
RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL)
RCT_REMAP_VIEW_PROPERTY(editable, textView.editable, BOOL)
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL) RCT_REMAP_VIEW_PROPERTY(color, textView.textColor, UIColor)
RCT_REMAP_VIEW_PROPERTY(editable, textView.editable, BOOL)
RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, textView.enablesReturnKeyAutomatically, BOOL)
RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType) RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType)
RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, textView.keyboardAppearance, UIKeyboardAppearance) RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, textView.keyboardAppearance, UIKeyboardAppearance)
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor)
RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType) RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType)
RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, textView.enablesReturnKeyAutomatically, BOOL) RCT_REMAP_VIEW_PROPERTY(secureTextEntry, textView.secureTextEntry, BOOL)
RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.autocapitalizationType, UITextAutocapitalizationType) RCT_EXPORT_VIEW_PROPERTY(text, NSString)
RCT_CUSTOM_VIEW_PROPERTY(fontSize, CGFloat, RCTTextView) RCT_CUSTOM_VIEW_PROPERTY(fontSize, CGFloat, RCTTextView)
{ {
view.font = [RCTConvert UIFont:view.font withSize:json ?: @(defaultView.font.pointSize)]; view.font = [RCTConvert UIFont:view.font withSize:json ?: @(defaultView.font.pointSize)];