Better TextInput: Fixing multiline <TextInput> insets and prepare for auto-expanding feature

Summary:
Several things:
 * The mess with insets was fixed. Previously we tried to compensate the insets difference with `UITextField` by adjusting `textContainerInset` property, moreover we delegated negative part of this compensation to the view inset. That was terrible because it breaks `contentSize` computation, complicates whole insets consept, complicates everything; it just was not right. Now we are fixing the top and left inset differences in different places. We disable left and right 5pt margin by setting `_textView.textContainer.lineFragmentPadding = 0` and we introduce top 5px inset as a DEFAULT value for top inset for common multiline <TextInput> (this value can be easilly overwritten in Javascript).
 * Internal layout and contentSize computations were unified and simplified.
 * Now we report `intrinsicContentSize` value to Yoga, one step before auto-expandable TextInput.

Depends on D4640207.

Reviewed By: mmmulani

Differential Revision: D4645921

fbshipit-source-id: da5988ebac50be967caecd71e780c014f6eb257a
This commit is contained in:
Valentin Shergin 2017-03-20 00:00:18 -07:00 committed by Facebook Github Bot
parent 3acafd1f3d
commit 1b013cd30c
5 changed files with 88 additions and 65 deletions

View File

@ -678,6 +678,7 @@ const TextInput = React.createClass({
if (props.inputView) {
children = [children, props.inputView];
}
props.style.unshift(styles.multilineInput);
textContainer =
<RCTTextView
ref={this._setNativeRef}
@ -867,6 +868,12 @@ var styles = StyleSheet.create({
input: {
alignSelf: 'stretch',
},
multilineInput: {
// This default top inset makes RCTTextView seem as close as possible
// to single-line RCTTextField defaults, using the system defaults
// of font size 17 and a height of 31 points.
paddingTop: 5,
},
});
module.exports = TextInput;

View File

@ -12,7 +12,7 @@
#import <React/RCTView.h>
#import <React/UIView+React.h>
@class RCTEventDispatcher;
@class RCTBridge;
@interface RCTTextView : RCTView <UITextViewDelegate>
@ -28,6 +28,7 @@
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, assign) NSInteger mostRecentEventCount;
@property (nonatomic, strong) NSNumber *maxLength;
@property (nonatomic, assign, readonly) CGSize contentSize;
@property (nonatomic, copy) RCTDirectEventBlock onChange;
@property (nonatomic, copy) RCTDirectEventBlock onContentSizeChange;
@ -35,7 +36,7 @@
@property (nonatomic, copy) RCTDirectEventBlock onTextInput;
@property (nonatomic, copy) RCTDirectEventBlock onScroll;
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
- (void)performTextUpdate;

View File

@ -11,6 +11,7 @@
#import <React/RCTConvert.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTUIManager.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
@ -69,6 +70,7 @@
@implementation RCTTextView
{
RCTBridge *_bridge;
RCTEventDispatcher *_eventDispatcher;
NSString *_placeholder;
@ -87,22 +89,25 @@
NSInteger _nativeEventCount;
CGSize _previousContentSize;
BOOL _viewDidCompleteInitialLayout;
}
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
RCTAssertParam(eventDispatcher);
RCTAssertParam(bridge);
if ((self = [super initWithFrame:CGRectZero])) {
if (self = [super initWithFrame:CGRectZero]) {
_contentInset = UIEdgeInsetsZero;
_eventDispatcher = eventDispatcher;
_bridge = bridge;
_eventDispatcher = bridge.eventDispatcher;
_placeholderTextColor = [self defaultPlaceholderTextColor];
_blurOnSubmit = NO;
_textView = [[RCTUITextView alloc] initWithFrame:CGRectZero];
_textView = [[RCTUITextView alloc] initWithFrame:self.bounds];
_textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_textView.backgroundColor = [UIColor clearColor];
_textView.textColor = [UIColor blackColor];
// This line actually removes 5pt (default value) left and right padding in UITextView.
_textView.textContainer.lineFragmentPadding = 0;
#if !TARGET_OS_TV
_textView.scrollsToTop = NO;
#endif
@ -132,7 +137,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
// If this <TextInput> is in rich text editing mode, and the child <Text> node providing rich text
// styling has a backgroundColor, then the attributedText produced by the child <Text> node will have an
// NSBackgroundColor attribute. We need to forward this attribute to the text view manually because the text view
// always has a clear background color in -initWithEventDispatcher:.
// always has a clear background color in `initWithBridge:`.
//
// TODO: This should be removed when the related hack in -performPendingTextUpdate is removed.
if (subview.backgroundColor) {
@ -237,60 +242,20 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
[_textView layoutIfNeeded];
[self updatePlaceholderVisibility];
[self updateContentSize];
[self invalidateContentSize];
_blockTextShouldChange = NO;
}
- (void)updateFrames
{
// Adjust the insets so that they are as close as possible to single-line
// RCTTextField defaults, using the system defaults of font size 17 and a
// height of 31 points.
//
// We apply the left inset to the frame since a negative left text-container
// inset mysteriously causes the text to be hidden until the text view is
// first focused.
UIEdgeInsets adjustedFrameInset = UIEdgeInsetsZero;
adjustedFrameInset.left = _contentInset.left - 5;
UIEdgeInsets adjustedTextContainerInset = _contentInset;
adjustedTextContainerInset.top += 5;
adjustedTextContainerInset.left = 0;
CGRect frame = UIEdgeInsetsInsetRect(self.bounds, adjustedFrameInset);
_textView.frame = frame;
_placeholderView.frame = frame;
[self updateContentSize];
_textView.textContainerInset = adjustedTextContainerInset;
_placeholderView.textContainerInset = adjustedTextContainerInset;
}
- (void)updateContentSize
{
CGSize size = _textView.frame.size;
size.height = [_textView sizeThatFits:CGSizeMake(size.width, INFINITY)].height;
if (_viewDidCompleteInitialLayout && _onContentSizeChange && !CGSizeEqualToSize(_previousContentSize, size)) {
_previousContentSize = size;
_onContentSizeChange(@{
@"contentSize": @{
@"height": @(size.height),
@"width": @(size.width),
},
@"target": self.reactTag,
});
}
}
- (void)updatePlaceholder
{
[_placeholderView removeFromSuperview];
_placeholderView = nil;
if (_placeholder) {
_placeholderView = [[UITextView alloc] initWithFrame:self.bounds];
_placeholderView = [[UITextView alloc] initWithFrame:_textView.frame];
_placeholderView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_placeholderView.textContainer.lineFragmentPadding = 0;
_placeholderView.userInteractionEnabled = NO;
_placeholderView.backgroundColor = [UIColor clearColor];
_placeholderView.scrollEnabled = NO;
@ -340,7 +305,9 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
- (void)setContentInset:(UIEdgeInsets)contentInset
{
_contentInset = contentInset;
[self updateFrames];
_textView.textContainerInset = contentInset;
_placeholderView.textContainerInset = contentInset;
[self setNeedsLayout];
}
#pragma mark - UITextViewDelegate
@ -503,8 +470,7 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
}
[self updatePlaceholderVisibility];
[self updateContentSize]; //keep the text wrapping when the length of
//the textline has been extended longer than the length of textinputView
[self invalidateContentSize];
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
}
@ -595,7 +561,7 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange,
- (void)textViewDidChange:(UITextView *)textView
{
[self updatePlaceholderVisibility];
[self updateContentSize];
[self invalidateContentSize];
// Detect when textView updates happend that didn't invoke `shouldChangeTextInRange`
// (e.g. typing simplified chinese in pinyin will insert and remove spaces without
@ -664,6 +630,8 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange,
eventCount:_nativeEventCount];
}
#pragma mark - UIResponder
- (BOOL)isFirstResponder
{
return [_textView isFirstResponder];
@ -695,17 +663,63 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange,
return [_textView resignFirstResponder];
}
#pragma mark - Content Size
- (CGSize)contentSize
{
// Returning value does NOT include insets.
CGSize contentSize = self.intrinsicContentSize;
contentSize.width -= _contentInset.left + _contentInset.right;
contentSize.height -= _contentInset.top + _contentInset.bottom;
return contentSize;
}
- (void)invalidateContentSize
{
CGSize contentSize = self.contentSize;
if (CGSizeEqualToSize(_previousContentSize, contentSize)) {
return;
}
_previousContentSize = contentSize;
[_bridge.uiManager setIntrinsicContentSize:contentSize forView:self];
if (_onContentSizeChange) {
_onContentSizeChange(@{
@"contentSize": @{
@"height": @(contentSize.height),
@"width": @(contentSize.width),
},
@"target": self.reactTag,
});
}
}
#pragma mark - Layout
- (CGSize)intrinsicContentSize
{
// Calling `sizeThatFits:` is probably more expensive method to compute
// content size compare to direct access `_textView.contentSize` property,
// but seems `sizeThatFits:` returns more reliable and consistent result.
// Returning value DOES include insets.
return [self sizeThatFits:CGSizeMake(self.bounds.size.width, INFINITY)];
}
- (CGSize)sizeThatFits:(CGSize)size
{
return [_textView sizeThatFits:size];
}
- (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];
[self invalidateContentSize];
}
#pragma mark - Default values
- (UIFont *)defaultPlaceholderFont
{
return [UIFont systemFontOfSize:17];

View File

@ -29,7 +29,7 @@ RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
return [[RCTTextView alloc] initWithBridge:self.bridge];
}
RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.autocapitalizationType, UITextAutocapitalizationType)

View File

@ -87,7 +87,8 @@ RCT_EXTERN NSString *const RCTUIManagerRootViewKey;
/**
* Set the natural size of a view, which is used when no explicit size is set.
* Use UIViewNoIntrinsicMetric to ignore a dimension.
* Use `UIViewNoIntrinsicMetric` to ignore a dimension.
* The `size` must NOT include padding and border.
*/
- (void)setIntrinsicContentSize:(CGSize)size forView:(UIView *)view;