iOS: Enable views to be nested within <Text>

Summary:
Previously, only Text and Image could be nested within Text. Now, any
view can be nested within Text. One restriction of this feature is
that developers must give inline views a width and a height via
the style prop.

Previously, inline Images were supported by using iOS's built-in support
for rendering images with an NSAttributedString via NSTextAttachment.
However, NSAttributedString doesn't support rendering arbitrary views.

This change adds support for nesting views within Text by creating one
NSTextAttachment per inline view. The NSTextAttachments act as placeholders.
They are set to be the size of the corresponding view. After the text is
laid out, we query the text system to find out where it has positioned each
NSTextAttachment. We then position the views to be at those locations.

This commit also contains a change in `RCTShadowText.m`
`_setParagraphStyleOnAttributedString:heightOfTallestSubview:`. It now only sets
`lineHeight`, `textAlign`, and `writingDirection` when they've actua
Closes https://github.com/facebook/react-native/pull/7304

Reviewed By: javache

Differential Revision: D3365373

Pulled By: nicklockwood

fbshipit-source-id: 66d149eb80c5c6725311e1e46d7323eec086ce64
This commit is contained in:
Adam Comella 2016-05-31 10:18:37 -07:00 committed by Facebook Github Bot 5
parent 5136d95f2c
commit 486dbe4e8f
16 changed files with 209 additions and 226 deletions

View File

@ -422,12 +422,13 @@ exports.examples = [
); );
}, },
}, { }, {
title: 'Inline images', title: 'Inline views',
render: function() { render: function() {
return ( return (
<View> <View>
<Text> <Text>
This text contains an inline image <Image source={require('./flux.png')} style={{width: 30, height: 11, resizeMode: 'cover'}}/>. Neat, huh? This text contains an inline blue view <View style={{width: 25, height: 25, backgroundColor: 'steelblue'}} /> and
an inline image <Image source={require('./flux.png')} style={{width: 30, height: 11, resizeMode: 'cover'}}/>. Neat, huh?
</Text> </Text>
</View> </View>
); );

View File

@ -201,10 +201,6 @@ var Image = React.createClass({
validAttributes: ReactNativeViewAttributes.UIView validAttributes: ReactNativeViewAttributes.UIView
}, },
contextTypes: {
isInAParentText: React.PropTypes.bool
},
render: function() { render: function() {
var source = resolveAssetSource(this.props.source) || {}; var source = resolveAssetSource(this.props.source) || {};
var {width, height, uri} = source; var {width, height, uri} = source;
@ -225,13 +221,6 @@ var Image = React.createClass({
console.warn('The <Image> component requires a `source` property rather than `src`.'); console.warn('The <Image> component requires a `source` property rather than `src`.');
} }
if (this.context.isInAParentText) {
RawImage = RCTVirtualImage;
if (!width || !height) {
console.warn('You must specify a width and height for the image %s', uri);
}
}
return ( return (
<RawImage <RawImage
{...this.props} {...this.props}
@ -252,7 +241,6 @@ var styles = StyleSheet.create({
var RCTImageView = requireNativeComponent('RCTImageView', Image); var RCTImageView = requireNativeComponent('RCTImageView', Image);
var RCTNetworkImageView = NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image) : RCTImageView; var RCTNetworkImageView = NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image) : RCTImageView;
var RCTVirtualImage = requireNativeComponent('RCTVirtualImage', Image);
module.exports = Image; module.exports = Image;

View File

@ -44,10 +44,6 @@
134B00A11B54232B00EC8DFB /* RCTImageUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtils.m; sourceTree = "<group>"; }; 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtils.m; sourceTree = "<group>"; };
139A38821C4D57AD00862840 /* RCTResizeMode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTResizeMode.h; sourceTree = "<group>"; }; 139A38821C4D57AD00862840 /* RCTResizeMode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTResizeMode.h; sourceTree = "<group>"; };
139A38831C4D587C00862840 /* RCTResizeMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTResizeMode.m; sourceTree = "<group>"; }; 139A38831C4D587C00862840 /* RCTResizeMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTResizeMode.m; sourceTree = "<group>"; };
13EF7F071BC42D4E003F47DD /* RCTShadowVirtualImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowVirtualImage.h; sourceTree = "<group>"; };
13EF7F081BC42D4E003F47DD /* RCTShadowVirtualImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowVirtualImage.m; sourceTree = "<group>"; };
13EF7F091BC42D4E003F47DD /* RCTVirtualImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVirtualImageManager.h; sourceTree = "<group>"; };
13EF7F0A1BC42D4E003F47DD /* RCTVirtualImageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTVirtualImageManager.m; sourceTree = "<group>"; };
13EF7F7D1BC825B1003F47DD /* RCTXCAssetImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTXCAssetImageLoader.h; sourceTree = "<group>"; }; 13EF7F7D1BC825B1003F47DD /* RCTXCAssetImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTXCAssetImageLoader.h; sourceTree = "<group>"; };
13EF7F7E1BC825B1003F47DD /* RCTXCAssetImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTXCAssetImageLoader.m; sourceTree = "<group>"; }; 13EF7F7E1BC825B1003F47DD /* RCTXCAssetImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTXCAssetImageLoader.m; sourceTree = "<group>"; };
143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = "<group>"; }; 143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = "<group>"; };
@ -95,10 +91,6 @@
35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */, 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */,
134B00A01B54232B00EC8DFB /* RCTImageUtils.h */, 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */,
134B00A11B54232B00EC8DFB /* RCTImageUtils.m */, 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */,
13EF7F071BC42D4E003F47DD /* RCTShadowVirtualImage.h */,
13EF7F081BC42D4E003F47DD /* RCTShadowVirtualImage.m */,
13EF7F091BC42D4E003F47DD /* RCTVirtualImageManager.h */,
13EF7F0A1BC42D4E003F47DD /* RCTVirtualImageManager.m */,
58B5115E1A9E6B3D00147676 /* Products */, 58B5115E1A9E6B3D00147676 /* Products */,
); );
indentWidth = 2; indentWidth = 2;
@ -169,7 +161,6 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
13EF7F0C1BC42D4E003F47DD /* RCTVirtualImageManager.m in Sources */,
35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */, 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */,
1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */, 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */,
1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */, 1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */,
@ -178,7 +169,6 @@
139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */, 139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */,
1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */, 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */,
EEF314721C9B0DD30049118E /* RCTImageBlurUtils.m in Sources */, EEF314721C9B0DD30049118E /* RCTImageBlurUtils.m in Sources */,
13EF7F0B1BC42D4E003F47DD /* RCTShadowVirtualImage.m in Sources */,
13EF7F7F1BC825B1003F47DD /* RCTXCAssetImageLoader.m in Sources */, 13EF7F7F1BC825B1003F47DD /* RCTXCAssetImageLoader.m in Sources */,
134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */, 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */,
); );

View File

@ -8,13 +8,12 @@
*/ */
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "RCTImageComponent.h"
#import "RCTResizeMode.h" #import "RCTResizeMode.h"
@class RCTBridge; @class RCTBridge;
@class RCTImageSource; @class RCTImageSource;
@interface RCTImageView : UIImageView <RCTImageComponent> @interface RCTImageView : UIImageView
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;

View File

@ -1,28 +0,0 @@
/**
* 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.
*/
#import "RCTShadowView.h"
#import "RCTImageComponent.h"
#import "RCTImageSource.h"
#import "RCTResizeMode.h"
@class RCTBridge;
/**
* Shadow image component, used for embedding images in non-view contexts such
* as text. This is NOT used for ordinary <Image> views.
*/
@interface RCTShadowVirtualImage : RCTShadowView <RCTImageComponent>
- (instancetype)initWithBridge:(RCTBridge *)bridge;
@property (nonatomic, strong) RCTImageSource *source;
@property (nonatomic, assign) RCTResizeMode resizeMode;
@end

View File

@ -1,82 +0,0 @@
/**
* 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.
*/
#import "RCTShadowVirtualImage.h"
#import "RCTImageLoader.h"
#import "RCTImageUtils.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTUIManager.h"
#import "RCTUtils.h"
@implementation RCTShadowVirtualImage
{
RCTBridge *_bridge;
RCTImageLoaderCancellationBlock _cancellationBlock;
}
@synthesize image = _image;
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if ((self = [super init])) {
_bridge = bridge;
}
return self;
}
RCT_NOT_IMPLEMENTED(-(instancetype)init)
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
if (changedProps.count == 0) {
// No need to reload image
return;
}
// Cancel previous request
if (_cancellationBlock) {
_cancellationBlock();
}
CGSize imageSize = {
RCTZeroIfNaN(self.width),
RCTZeroIfNaN(self.height),
};
__weak RCTShadowVirtualImage *weakSelf = self;
_cancellationBlock = [_bridge.imageLoader loadImageWithTag:_source.imageURL.absoluteString
size:imageSize
scale:RCTScreenScale()
resizeMode:_resizeMode
progressBlock:nil
completionBlock:^(NSError *error, UIImage *image) {
dispatch_async(_bridge.uiManager.methodQueue, ^{
RCTShadowVirtualImage *strongSelf = weakSelf;
if (![_source isEqual:strongSelf.source]) {
// Bail out if source has changed since we started loading
return;
}
strongSelf->_image = image;
[strongSelf dirtyText];
});
}];
}
- (void)dealloc
{
if (_cancellationBlock) {
_cancellationBlock();
}
}
@end

View File

@ -1,14 +0,0 @@
/**
* 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.
*/
#import "RCTViewManager.h"
@interface RCTVirtualImageManager : RCTViewManager
@end

View File

@ -1,25 +0,0 @@
/**
* 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.
*/
#import "RCTVirtualImageManager.h"
#import "RCTShadowVirtualImage.h"
@implementation RCTVirtualImageManager
RCT_EXPORT_MODULE()
- (RCTShadowView *)shadowView
{
return [[RCTShadowVirtualImage alloc] initWithBridge:self.bridge];
}
RCT_EXPORT_SHADOW_PROPERTY(source, RCTImageSource)
RCT_EXPORT_SHADOW_PROPERTY(resizeMode, UIViewContentMode)
@end

View File

@ -13,12 +13,12 @@
#import "RCTUIManager.h" #import "RCTUIManager.h"
#import "RCTBridge.h" #import "RCTBridge.h"
#import "RCTConvert.h" #import "RCTConvert.h"
#import "RCTImageComponent.h"
#import "RCTLog.h" #import "RCTLog.h"
#import "RCTShadowRawText.h" #import "RCTShadowRawText.h"
#import "RCTText.h" #import "RCTText.h"
#import "RCTUtils.h" #import "RCTUtils.h"
NSString *const RCTShadowViewAttributeName = @"RCTShadowViewAttributeName";
NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName"; NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName";
NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName"; NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName";
@ -114,6 +114,45 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width
[self dirtyPropagation]; [self dirtyPropagation];
} }
- (void)applyLayoutToChildren:(css_node_t *)node
viewsWithNewFrame:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame
absolutePosition:(CGPoint)absolutePosition
{
// Run layout on subviews.
NSTextStorage *textStorage = [self buildTextStorageForWidth:self.frame.size.width widthMode:CSS_MEASURE_MODE_EXACTLY];
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
[layoutManager.textStorage enumerateAttribute:RCTShadowViewAttributeName inRange:characterRange options:0 usingBlock:^(RCTShadowView *child, NSRange range, BOOL *_) {
if (child) {
css_node_t *childNode = child.cssNode;
float width = childNode->style.dimensions[CSS_WIDTH];
float height = childNode->style.dimensions[CSS_HEIGHT];
if (isUndefined(width) || isUndefined(height)) {
RCTLogError(@"Views nested within a <Text> must have a width and height");
}
UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil];
CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer];
CGRect childFrame = {{
RCTRoundPixelValue(glyphRect.origin.x),
RCTRoundPixelValue(glyphRect.origin.y + glyphRect.size.height - height + font.descender)
}, {
RCTRoundPixelValue(width),
RCTRoundPixelValue(height)
}};
NSRange truncatedGlyphRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:range.location];
BOOL childIsTruncated = NSIntersectionRange(range, truncatedGlyphRange).length != 0;
[child collectUpdatedFrames:viewsWithNewFrame
withFrame:childFrame
hidden:childIsTruncated
absolutePosition:absolutePosition];
}
}];
}
- (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode
{ {
if (_cachedTextStorage && width == _cachedTextStorageWidth && widthMode == _cachedTextStorageWidthMode) { if (_cachedTextStorage && width == _cachedTextStorageWidth && widthMode == _cachedTextStorageWidthMode) {
@ -199,33 +238,48 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width
_effectiveLetterSpacing = letterSpacing.doubleValue; _effectiveLetterSpacing = letterSpacing.doubleValue;
UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily
size:fontSize weight:fontWeight style:fontStyle
scaleMultiplier:_allowFontScaling ? _fontSizeMultiplier : 1.0];
CGFloat heightOfTallestSubview = 0.0;
NSMutableAttributedString *attributedString = [NSMutableAttributedString new]; NSMutableAttributedString *attributedString = [NSMutableAttributedString new];
for (RCTShadowView *child in [self reactSubviews]) { for (RCTShadowView *child in [self reactSubviews]) {
if ([child isKindOfClass:[RCTShadowText class]]) { if ([child isKindOfClass:[RCTShadowText class]]) {
RCTShadowText *shadowText = (RCTShadowText *)child; RCTShadowText *shadowText = (RCTShadowText *)child;
[attributedString appendAttributedString: [attributedString appendAttributedString:
[shadowText _attributedStringWithFontFamily:fontFamily [shadowText _attributedStringWithFontFamily:fontFamily
fontSize:fontSize fontSize:fontSize
fontWeight:fontWeight fontWeight:fontWeight
fontStyle:fontStyle fontStyle:fontStyle
letterSpacing:letterSpacing letterSpacing:letterSpacing
useBackgroundColor:YES useBackgroundColor:YES
foregroundColor:shadowText.color ?: foregroundColor foregroundColor:shadowText.color ?: foregroundColor
backgroundColor:shadowText.backgroundColor ?: backgroundColor backgroundColor:shadowText.backgroundColor ?: backgroundColor
opacity:opacity * shadowText.opacity]]; opacity:opacity * shadowText.opacity]];
[child setTextComputed];
} else if ([child isKindOfClass:[RCTShadowRawText class]]) { } else if ([child isKindOfClass:[RCTShadowRawText class]]) {
RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child; RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child;
[attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:shadowRawText.text ?: @""]]; [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:shadowRawText.text ?: @""]];
} else if ([child conformsToProtocol:@protocol(RCTImageComponent)]) { [child setTextComputed];
NSTextAttachment *imageAttachment = [NSTextAttachment new];
imageAttachment.image = ((id<RCTImageComponent>)child).image;
imageAttachment.bounds = (CGRect){CGPointZero, {RCTZeroIfNaN(child.width), RCTZeroIfNaN(child.height)}};
[attributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:imageAttachment]];
} else { } else {
RCTLogError(@"<Text> can't have any children except <Text>, <Image> or raw strings"); float width = child.cssNode->style.dimensions[CSS_WIDTH];
float height = child.cssNode->style.dimensions[CSS_HEIGHT];
if (isUndefined(width) || isUndefined(height)) {
RCTLogError(@"Views nested within a <Text> must have a width and height");
}
NSTextAttachment *attachment = [NSTextAttachment new];
attachment.bounds = (CGRect){CGPointZero, {width, height}};
NSMutableAttributedString *attachmentString = [NSMutableAttributedString new];
[attachmentString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
[attachmentString addAttribute:RCTShadowViewAttributeName value:child range:(NSRange){0, attachmentString.length}];
[attributedString appendAttributedString:attachmentString];
if (height > heightOfTallestSubview) {
heightOfTallestSubview = height;
}
// Don't call setTextComputed on this child. RCTTextManager takes care of
// processing inline UIViews.
} }
[child setTextComputed];
} }
[self _addAttribute:NSForegroundColorAttributeName [self _addAttribute:NSForegroundColorAttributeName
@ -241,13 +295,10 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width
toAttributedString:attributedString]; toAttributedString:attributedString];
} }
UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily
size:fontSize weight:fontWeight style:fontStyle
scaleMultiplier:_allowFontScaling ? _fontSizeMultiplier : 1.0];
[self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString]; [self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString];
[self _addAttribute:NSKernAttributeName withValue:letterSpacing toAttributedString:attributedString]; [self _addAttribute:NSKernAttributeName withValue:letterSpacing toAttributedString:attributedString];
[self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString]; [self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString];
[self _setParagraphStyleOnAttributedString:attributedString]; [self _setParagraphStyleOnAttributedString:attributedString heightOfTallestSubview:heightOfTallestSubview];
// create a non-mutable attributedString for use by the Text system which avoids copies down the line // create a non-mutable attributedString for use by the Text system which avoids copies down the line
_cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:attributedString]; _cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:attributedString];
@ -270,6 +321,7 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width
* varying lineHeights, we simply take the max. * varying lineHeights, we simply take the max.
*/ */
- (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attributedString - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attributedString
heightOfTallestSubview:(CGFloat)heightOfTallestSubview
{ {
// check if we have lineHeight set on self // check if we have lineHeight set on self
__block BOOL hasParagraphStyle = NO; __block BOOL hasParagraphStyle = NO;
@ -277,9 +329,7 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width
hasParagraphStyle = YES; hasParagraphStyle = YES;
} }
if (!_lineHeight) { __block float newLineHeight = _lineHeight ?: 0.0;
self.lineHeight = 0.0;
}
CGFloat fontSizeMultiplier = _allowFontScaling ? _fontSizeMultiplier : 1.0; CGFloat fontSizeMultiplier = _allowFontScaling ? _fontSizeMultiplier : 1.0;
@ -288,15 +338,25 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width
if (value) { if (value) {
NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value; NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value;
CGFloat maximumLineHeight = round(paragraphStyle.maximumLineHeight / fontSizeMultiplier); CGFloat maximumLineHeight = round(paragraphStyle.maximumLineHeight / fontSizeMultiplier);
if (maximumLineHeight > self.lineHeight) { if (maximumLineHeight > newLineHeight) {
self.lineHeight = maximumLineHeight; newLineHeight = maximumLineHeight;
} }
hasParagraphStyle = YES; hasParagraphStyle = YES;
} }
}]; }];
self.textAlign = _textAlign ?: NSTextAlignmentNatural; if (self.lineHeight != newLineHeight) {
self.writingDirection = _writingDirection ?: NSWritingDirectionNatural; self.lineHeight = newLineHeight;
}
NSTextAlignment newTextAlign = _textAlign ?: NSTextAlignmentNatural;
if (self.textAlign != newTextAlign) {
self.textAlign = newTextAlign;
}
NSWritingDirection newWritingDirection = _writingDirection ?: NSWritingDirectionNatural;
if (self.writingDirection != newWritingDirection) {
self.writingDirection = newWritingDirection;
}
// if we found anything, set it :D // if we found anything, set it :D
if (hasParagraphStyle) { if (hasParagraphStyle) {
@ -304,6 +364,9 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width
paragraphStyle.alignment = _textAlign; paragraphStyle.alignment = _textAlign;
paragraphStyle.baseWritingDirection = _writingDirection; paragraphStyle.baseWritingDirection = _writingDirection;
CGFloat lineHeight = round(_lineHeight * fontSizeMultiplier); CGFloat lineHeight = round(_lineHeight * fontSizeMultiplier);
if (heightOfTallestSubview > lineHeight) {
lineHeight = ceilf(heightOfTallestSubview);
}
paragraphStyle.minimumLineHeight = lineHeight; paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.maximumLineHeight = lineHeight; paragraphStyle.maximumLineHeight = lineHeight;
[attributedString addAttribute:NSParagraphStyleAttributeName [attributedString addAttribute:NSParagraphStyleAttributeName

View File

@ -13,6 +13,17 @@
#import "RCTUtils.h" #import "RCTUtils.h"
#import "UIView+React.h" #import "UIView+React.h"
static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDescendants)
{
for (UIView *child in view.reactSubviews) {
if ([child isKindOfClass:[RCTText class]]) {
collectNonTextDescendants((RCTText *)child, nonTextDescendants);
} else if (!CGRectEqualToRect(child.frame, CGRectZero)) {
[nonTextDescendants addObject:child];
}
}
}
@implementation RCTText @implementation RCTText
{ {
NSTextStorage *_textStorage; NSTextStorage *_textStorage;
@ -76,6 +87,21 @@
{ {
if (_textStorage != textStorage) { if (_textStorage != textStorage) {
_textStorage = textStorage; _textStorage = textStorage;
NSMutableArray *nonTextDescendants = [NSMutableArray new];
collectNonTextDescendants(self, nonTextDescendants);
NSArray *subviews = self.subviews;
if (![subviews isEqualToArray:nonTextDescendants]) {
for (UIView *child in subviews) {
if (![nonTextDescendants containsObject:child]) {
[child removeFromSuperview];
}
}
for (UIView *child in nonTextDescendants) {
[self addSubview:child];
}
}
[self setNeedsDisplay]; [self setNeedsDisplay];
} }
} }

View File

@ -20,6 +20,18 @@
#import "RCTTextView.h" #import "RCTTextView.h"
#import "UIView+React.h" #import "UIView+React.h"
static void collectDirtyNonTextDescendants(RCTShadowText *shadowView, NSMutableArray *nonTextDescendants) {
for (RCTShadowView *child in shadowView.reactSubviews) {
if ([child isKindOfClass:[RCTShadowText class]]) {
collectDirtyNonTextDescendants((RCTShadowText *)child, nonTextDescendants);
} else if ([child isKindOfClass:[RCTShadowRawText class]]) {
// no-op
} else if ([child isTextDirty]) {
[nonTextDescendants addObject:child];
}
}
}
@interface RCTShadowText (Private) @interface RCTShadowText (Private)
- (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode; - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode;
@ -85,6 +97,7 @@ RCT_EXPORT_SHADOW_PROPERTY(textShadowColor, UIColor)
if ([shadowView isKindOfClass:[RCTShadowText class]]) { if ([shadowView isKindOfClass:[RCTShadowText class]]) {
((RCTShadowText *)shadowView).fontSizeMultiplier = self.bridge.accessibilityManager.multiplier; ((RCTShadowText *)shadowView).fontSizeMultiplier = self.bridge.accessibilityManager.multiplier;
[(RCTShadowText *)shadowView recomputeText]; [(RCTShadowText *)shadowView recomputeText];
collectDirtyNonTextDescendants((RCTShadowText *)shadowView, queue);
} else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) { } else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) {
RCTLogError(@"Raw text cannot be used outside of a <Text> tag. Not rendering string: '%@'", RCTLogError(@"Raw text cannot be used outside of a <Text> tag. Not rendering string: '%@'",
[(RCTShadowRawText *)shadowView text]); [(RCTShadowRawText *)shadowView text]);

View File

@ -217,7 +217,6 @@
13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = "<group>"; }; 13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = "<group>"; };
13E067531A70F44B002CDEE1 /* UIView+React.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+React.h"; sourceTree = "<group>"; }; 13E067531A70F44B002CDEE1 /* UIView+React.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+React.h"; sourceTree = "<group>"; };
13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = "<group>"; }; 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = "<group>"; };
13EF7F441BC69646003F47DD /* RCTImageComponent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageComponent.h; sourceTree = "<group>"; };
13F17A831B8493E5007D4C75 /* RCTRedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRedBox.h; sourceTree = "<group>"; }; 13F17A831B8493E5007D4C75 /* RCTRedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRedBox.h; sourceTree = "<group>"; };
13F17A841B8493E5007D4C75 /* RCTRedBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRedBox.m; sourceTree = "<group>"; }; 13F17A841B8493E5007D4C75 /* RCTRedBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRedBox.m; sourceTree = "<group>"; };
14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = "<group>"; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = "<group>"; };
@ -388,7 +387,6 @@
13AB90BF1B6FA36700713B4F /* RCTComponentData.h */, 13AB90BF1B6FA36700713B4F /* RCTComponentData.h */,
13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */, 13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */,
13AB90C01B6FA36700713B4F /* RCTComponentData.m */, 13AB90C01B6FA36700713B4F /* RCTComponentData.m */,
13EF7F441BC69646003F47DD /* RCTImageComponent.h */,
13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */, 13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */,
13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */, 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */,
13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */, 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */,

View File

@ -1,19 +0,0 @@
/**
* 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.
*/
#import <UIKit/UIKit.h>
/**
* Generic interface for components that contain an image.
*/
@protocol RCTImageComponent <NSObject>
@property (nonatomic, strong, readonly) UIImage *image;
@end

View File

@ -140,12 +140,33 @@ typedef void (^RCTApplierBlock)(NSDictionary<NSNumber *, UIView *> *viewRegistry
parentProperties:(NSDictionary<NSString *, id> *)parentProperties NS_REQUIRES_SUPER; parentProperties:(NSDictionary<NSString *, id> *)parentProperties NS_REQUIRES_SUPER;
/** /**
* Recursively apply layout to children. * Can be called by a parent on a child in order to calculate all views whose frame needs
* updating in that branch. Adds these frames to `viewsWithNewFrame`. Useful if layout
* enters a view where flex doesn't apply (e.g. Text) and then you want to resume flex
* layout on a subview.
*/
- (void)collectUpdatedFrames:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame
withFrame:(CGRect)frame
hidden:(BOOL)hidden
absolutePosition:(CGPoint)absolutePosition;
/**
* Apply the CSS layout.
* This method also calls `applyLayoutToChildren:` internally. The functionality
* is split into two methods so subclasses can override `applyLayoutToChildren:`
* while using default implementation of `applyLayoutNode:`.
*/ */
- (void)applyLayoutNode:(css_node_t *)node - (void)applyLayoutNode:(css_node_t *)node
viewsWithNewFrame:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame viewsWithNewFrame:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame
absolutePosition:(CGPoint)absolutePosition NS_REQUIRES_SUPER; absolutePosition:(CGPoint)absolutePosition NS_REQUIRES_SUPER;
/**
* Enumerate the child nodes and tell them to apply layout.
*/
- (void)applyLayoutToChildren:(css_node_t *)node
viewsWithNewFrame:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame
absolutePosition:(CGPoint)absolutePosition;
/** /**
* The following are implementation details exposed to subclasses. Do not call them directly * The following are implementation details exposed to subclasses. Do not call them directly
*/ */

View File

@ -157,6 +157,13 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
absolutePosition.x += node->layout.position[CSS_LEFT]; absolutePosition.x += node->layout.position[CSS_LEFT];
absolutePosition.y += node->layout.position[CSS_TOP]; absolutePosition.y += node->layout.position[CSS_TOP];
[self applyLayoutToChildren:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition];
}
- (void)applyLayoutToChildren:(css_node_t *)node
viewsWithNewFrame:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame
absolutePosition:(CGPoint)absolutePosition
{
for (int i = 0; i < node->children_count; ++i) { for (int i = 0; i < node->children_count; ++i) {
RCTShadowView *child = (RCTShadowView *)_reactSubviews[i]; RCTShadowView *child = (RCTShadowView *)_reactSubviews[i];
[child applyLayoutNode:node->get_child(node->context, i) [child applyLayoutNode:node->get_child(node->context, i)
@ -209,6 +216,36 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
} }
} }
- (void)collectUpdatedFrames:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame
withFrame:(CGRect)frame
hidden:(BOOL)hidden
absolutePosition:(CGPoint)absolutePosition
{
if (_hidden != hidden) {
// The hidden state has changed. Even if the frame hasn't changed, add
// this ShadowView to viewsWithNewFrame so the UIManager will process
// this ShadowView's UIView and update its hidden state.
_hidden = hidden;
[viewsWithNewFrame addObject:self];
}
if (!CGRectEqualToRect(frame, _frame)) {
_cssNode->style.position_type = CSS_POSITION_ABSOLUTE;
_cssNode->style.dimensions[CSS_WIDTH] = frame.size.width;
_cssNode->style.dimensions[CSS_HEIGHT] = frame.size.height;
_cssNode->style.position[CSS_LEFT] = frame.origin.x;
_cssNode->style.position[CSS_TOP] = frame.origin.y;
// Our parent has asked us to change our cssNode->styles. Dirty the layout
// so that we can rerun layout on this node. The request came from our parent
// so there's no need to dirty our ancestors by calling dirtyLayout.
_layoutLifecycle = RCTUpdateLifecycleDirtied;
}
[self fillCSSNode:_cssNode];
layoutNode(_cssNode, frame.size.width, frame.size.height, CSS_DIRECTION_INHERIT);
[self applyLayoutNode:_cssNode viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition];
}
- (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor - (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor
{ {
CGPoint offset = CGPointZero; CGPoint offset = CGPointZero;
@ -459,6 +496,7 @@ RCT_BORDER_PROPERTY(Right, RIGHT)
{ \ { \
_cssNode->style.dimensions[CSS_##cssProp] = value; \ _cssNode->style.dimensions[CSS_##cssProp] = value; \
[self dirtyLayout]; \ [self dirtyLayout]; \
[self dirtyText]; \
} \ } \
- (CGFloat)getProp \ - (CGFloat)getProp \
{ \ { \

View File

@ -21,6 +21,20 @@ Behind the scenes, React Native converts this to a flat `NSAttributedString` or
9-17: bold, red 9-17: bold, red
``` ```
## Nested Views (iOS Only)
On iOS, you can nest views within your Text component. Here's an example:
```javascript
<Text>
There is a blue square
<View style={{width: 50, height: 50, backgroundColor: 'steelblue'}} />
in between my text.
</Text>
```
In order to use this feature, you must give the view a `width` and a `height`.
## Containers ## Containers
The `<Text>` element is special relative to layout: everything inside is no longer using the flexbox layout but using text layout. This means that elements inside of a `<Text>` are no longer rectangles, but wrap when they see the end of the line. The `<Text>` element is special relative to layout: everything inside is no longer using the flexbox layout but using text layout. This means that elements inside of a `<Text>` are no longer rectangles, but wrap when they see the end of the line.