From 486dbe4e8f4e3710c258e22af00b2aa7155ef2f3 Mon Sep 17 00:00:00 2001 From: Adam Comella Date: Tue, 31 May 2016 10:18:37 -0700 Subject: [PATCH] iOS: Enable views to be nested within 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 --- Examples/UIExplorer/TextExample.ios.js | 5 +- Libraries/Image/Image.ios.js | 12 -- .../Image/RCTImage.xcodeproj/project.pbxproj | 10 -- Libraries/Image/RCTImageView.h | 3 +- Libraries/Image/RCTShadowVirtualImage.h | 28 ---- Libraries/Image/RCTShadowVirtualImage.m | 82 ------------ Libraries/Image/RCTVirtualImageManager.h | 14 -- Libraries/Image/RCTVirtualImageManager.m | 25 ---- Libraries/Text/RCTShadowText.m | 121 +++++++++++++----- Libraries/Text/RCTText.m | 26 ++++ Libraries/Text/RCTTextManager.m | 13 ++ React/React.xcodeproj/project.pbxproj | 2 - React/Views/RCTImageComponent.h | 19 --- React/Views/RCTShadowView.h | 23 +++- React/Views/RCTShadowView.m | 38 ++++++ docs/Text.md | 14 ++ 16 files changed, 209 insertions(+), 226 deletions(-) delete mode 100644 Libraries/Image/RCTShadowVirtualImage.h delete mode 100644 Libraries/Image/RCTShadowVirtualImage.m delete mode 100644 Libraries/Image/RCTVirtualImageManager.h delete mode 100644 Libraries/Image/RCTVirtualImageManager.m delete mode 100644 React/Views/RCTImageComponent.h diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index e21b7c3bf..8c7b4ff86 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -422,12 +422,13 @@ exports.examples = [ ); }, }, { - title: 'Inline images', + title: 'Inline views', render: function() { return ( - This text contains an inline image . Neat, huh? + This text contains an inline blue view and + an inline image . Neat, huh? ); diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 7c4966568..b2f4718f7 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -201,10 +201,6 @@ var Image = React.createClass({ validAttributes: ReactNativeViewAttributes.UIView }, - contextTypes: { - isInAParentText: React.PropTypes.bool - }, - render: function() { var source = resolveAssetSource(this.props.source) || {}; var {width, height, uri} = source; @@ -225,13 +221,6 @@ var Image = React.createClass({ console.warn('The 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 ( -#import "RCTImageComponent.h" #import "RCTResizeMode.h" @class RCTBridge; @class RCTImageSource; -@interface RCTImageView : UIImageView +@interface RCTImageView : UIImageView - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Image/RCTShadowVirtualImage.h b/Libraries/Image/RCTShadowVirtualImage.h deleted file mode 100644 index b9623f736..000000000 --- a/Libraries/Image/RCTShadowVirtualImage.h +++ /dev/null @@ -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 views. - */ -@interface RCTShadowVirtualImage : RCTShadowView - -- (instancetype)initWithBridge:(RCTBridge *)bridge; - -@property (nonatomic, strong) RCTImageSource *source; -@property (nonatomic, assign) RCTResizeMode resizeMode; - -@end diff --git a/Libraries/Image/RCTShadowVirtualImage.m b/Libraries/Image/RCTShadowVirtualImage.m deleted file mode 100644 index 757a48c24..000000000 --- a/Libraries/Image/RCTShadowVirtualImage.m +++ /dev/null @@ -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 *)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 diff --git a/Libraries/Image/RCTVirtualImageManager.h b/Libraries/Image/RCTVirtualImageManager.h deleted file mode 100644 index b92896235..000000000 --- a/Libraries/Image/RCTVirtualImageManager.h +++ /dev/null @@ -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 diff --git a/Libraries/Image/RCTVirtualImageManager.m b/Libraries/Image/RCTVirtualImageManager.m deleted file mode 100644 index 6311010f4..000000000 --- a/Libraries/Image/RCTVirtualImageManager.m +++ /dev/null @@ -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 diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index adf7be667..eced9cd0c 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -13,12 +13,12 @@ #import "RCTUIManager.h" #import "RCTBridge.h" #import "RCTConvert.h" -#import "RCTImageComponent.h" #import "RCTLog.h" #import "RCTShadowRawText.h" #import "RCTText.h" #import "RCTUtils.h" +NSString *const RCTShadowViewAttributeName = @"RCTShadowViewAttributeName"; NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName"; NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName"; @@ -114,6 +114,45 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width [self dirtyPropagation]; } +- (void)applyLayoutToChildren:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)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 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 { 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; + 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]; for (RCTShadowView *child in [self reactSubviews]) { if ([child isKindOfClass:[RCTShadowText class]]) { RCTShadowText *shadowText = (RCTShadowText *)child; [attributedString appendAttributedString: - [shadowText _attributedStringWithFontFamily:fontFamily - fontSize:fontSize - fontWeight:fontWeight - fontStyle:fontStyle - letterSpacing:letterSpacing - useBackgroundColor:YES - foregroundColor:shadowText.color ?: foregroundColor - backgroundColor:shadowText.backgroundColor ?: backgroundColor - opacity:opacity * shadowText.opacity]]; + [shadowText _attributedStringWithFontFamily:fontFamily + fontSize:fontSize + fontWeight:fontWeight + fontStyle:fontStyle + letterSpacing:letterSpacing + useBackgroundColor:YES + foregroundColor:shadowText.color ?: foregroundColor + backgroundColor:shadowText.backgroundColor ?: backgroundColor + opacity:opacity * shadowText.opacity]]; + [child setTextComputed]; } else if ([child isKindOfClass:[RCTShadowRawText class]]) { RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child; [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:shadowRawText.text ?: @""]]; - } else if ([child conformsToProtocol:@protocol(RCTImageComponent)]) { - NSTextAttachment *imageAttachment = [NSTextAttachment new]; - imageAttachment.image = ((id)child).image; - imageAttachment.bounds = (CGRect){CGPointZero, {RCTZeroIfNaN(child.width), RCTZeroIfNaN(child.height)}}; - [attributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:imageAttachment]]; + [child setTextComputed]; } else { - RCTLogError(@" can't have any children except , 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 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 @@ -241,13 +295,10 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width 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:NSKernAttributeName withValue:letterSpacing 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 _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. */ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attributedString + heightOfTallestSubview:(CGFloat)heightOfTallestSubview { // check if we have lineHeight set on self __block BOOL hasParagraphStyle = NO; @@ -277,9 +329,7 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width hasParagraphStyle = YES; } - if (!_lineHeight) { - self.lineHeight = 0.0; - } + __block float newLineHeight = _lineHeight ?: 0.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) { NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value; CGFloat maximumLineHeight = round(paragraphStyle.maximumLineHeight / fontSizeMultiplier); - if (maximumLineHeight > self.lineHeight) { - self.lineHeight = maximumLineHeight; + if (maximumLineHeight > newLineHeight) { + newLineHeight = maximumLineHeight; } hasParagraphStyle = YES; } }]; - self.textAlign = _textAlign ?: NSTextAlignmentNatural; - self.writingDirection = _writingDirection ?: NSWritingDirectionNatural; + if (self.lineHeight != newLineHeight) { + 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 (hasParagraphStyle) { @@ -304,6 +364,9 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width paragraphStyle.alignment = _textAlign; paragraphStyle.baseWritingDirection = _writingDirection; CGFloat lineHeight = round(_lineHeight * fontSizeMultiplier); + if (heightOfTallestSubview > lineHeight) { + lineHeight = ceilf(heightOfTallestSubview); + } paragraphStyle.minimumLineHeight = lineHeight; paragraphStyle.maximumLineHeight = lineHeight; [attributedString addAttribute:NSParagraphStyleAttributeName diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index e57b93aa1..864fa096b 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -13,6 +13,17 @@ #import "RCTUtils.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 { NSTextStorage *_textStorage; @@ -76,6 +87,21 @@ { if (_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]; } } diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index 92fa58949..9104ea72c 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -20,6 +20,18 @@ #import "RCTTextView.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) - (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]]) { ((RCTShadowText *)shadowView).fontSizeMultiplier = self.bridge.accessibilityManager.multiplier; [(RCTShadowText *)shadowView recomputeText]; + collectDirtyNonTextDescendants((RCTShadowText *)shadowView, queue); } else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) { RCTLogError(@"Raw text cannot be used outside of a tag. Not rendering string: '%@'", [(RCTShadowRawText *)shadowView text]); diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index c43d45fe5..f103966c3 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -217,7 +217,6 @@ 13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = ""; }; 13E067531A70F44B002CDEE1 /* UIView+React.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+React.h"; sourceTree = ""; }; 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = ""; }; - 13EF7F441BC69646003F47DD /* RCTImageComponent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageComponent.h; sourceTree = ""; }; 13F17A831B8493E5007D4C75 /* RCTRedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRedBox.h; sourceTree = ""; }; 13F17A841B8493E5007D4C75 /* RCTRedBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRedBox.m; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; @@ -388,7 +387,6 @@ 13AB90BF1B6FA36700713B4F /* RCTComponentData.h */, 13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */, 13AB90C01B6FA36700713B4F /* RCTComponentData.m */, - 13EF7F441BC69646003F47DD /* RCTImageComponent.h */, 13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */, 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */, 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */, diff --git a/React/Views/RCTImageComponent.h b/React/Views/RCTImageComponent.h deleted file mode 100644 index a6916c9aa..000000000 --- a/React/Views/RCTImageComponent.h +++ /dev/null @@ -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 - -/** - * Generic interface for components that contain an image. - */ -@protocol RCTImageComponent - -@property (nonatomic, strong, readonly) UIImage *image; - -@end diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index ad09ae9f0..67955a9bc 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -140,12 +140,33 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry parentProperties:(NSDictionary *)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 *)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 viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition NS_REQUIRES_SUPER; +/** + * Enumerate the child nodes and tell them to apply layout. + */ +- (void)applyLayoutToChildren:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition; + /** * The following are implementation details exposed to subclasses. Do not call them directly */ diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index d55e80c62..83ce22a5b 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -157,6 +157,13 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st absolutePosition.x += node->layout.position[CSS_LEFT]; absolutePosition.y += node->layout.position[CSS_TOP]; + [self applyLayoutToChildren:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; +} + +- (void)applyLayoutToChildren:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition +{ for (int i = 0; i < node->children_count; ++i) { RCTShadowView *child = (RCTShadowView *)_reactSubviews[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 *)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 { CGPoint offset = CGPointZero; @@ -459,6 +496,7 @@ RCT_BORDER_PROPERTY(Right, RIGHT) { \ _cssNode->style.dimensions[CSS_##cssProp] = value; \ [self dirtyLayout]; \ + [self dirtyText]; \ } \ - (CGFloat)getProp \ { \ diff --git a/docs/Text.md b/docs/Text.md index 800137b03..48853606e 100644 --- a/docs/Text.md +++ b/docs/Text.md @@ -21,6 +21,20 @@ Behind the scenes, React Native converts this to a flat `NSAttributedString` or 9-17: bold, red ``` +## Nested Views (iOS Only) + +On iOS, you can nest views within your Text component. Here's an example: + +```javascript + + There is a blue square + + in between my text. + +``` + +In order to use this feature, you must give the view a `width` and a `height`. + ## Containers The `` 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 `` are no longer rectangles, but wrap when they see the end of the line.