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:
parent
5136d95f2c
commit
486dbe4e8f
|
@ -422,12 +422,13 @@ exports.examples = [
|
|||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Inline images',
|
||||
title: 'Inline views',
|
||||
render: function() {
|
||||
return (
|
||||
<View>
|
||||
<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>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -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 <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 (
|
||||
<RawImage
|
||||
{...this.props}
|
||||
|
@ -252,7 +241,6 @@ var styles = StyleSheet.create({
|
|||
|
||||
var RCTImageView = requireNativeComponent('RCTImageView', Image);
|
||||
var RCTNetworkImageView = NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image) : RCTImageView;
|
||||
var RCTVirtualImage = requireNativeComponent('RCTVirtualImage', Image);
|
||||
|
||||
|
||||
module.exports = Image;
|
||||
|
|
|
@ -44,10 +44,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -95,10 +91,6 @@
|
|||
35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */,
|
||||
134B00A01B54232B00EC8DFB /* RCTImageUtils.h */,
|
||||
134B00A11B54232B00EC8DFB /* RCTImageUtils.m */,
|
||||
13EF7F071BC42D4E003F47DD /* RCTShadowVirtualImage.h */,
|
||||
13EF7F081BC42D4E003F47DD /* RCTShadowVirtualImage.m */,
|
||||
13EF7F091BC42D4E003F47DD /* RCTVirtualImageManager.h */,
|
||||
13EF7F0A1BC42D4E003F47DD /* RCTVirtualImageManager.m */,
|
||||
58B5115E1A9E6B3D00147676 /* Products */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
|
@ -169,7 +161,6 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
13EF7F0C1BC42D4E003F47DD /* RCTVirtualImageManager.m in Sources */,
|
||||
35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */,
|
||||
1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */,
|
||||
1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */,
|
||||
|
@ -178,7 +169,6 @@
|
|||
139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */,
|
||||
1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */,
|
||||
EEF314721C9B0DD30049118E /* RCTImageBlurUtils.m in Sources */,
|
||||
13EF7F0B1BC42D4E003F47DD /* RCTShadowVirtualImage.m in Sources */,
|
||||
13EF7F7F1BC825B1003F47DD /* RCTXCAssetImageLoader.m in Sources */,
|
||||
134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */,
|
||||
);
|
||||
|
|
|
@ -8,13 +8,12 @@
|
|||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "RCTImageComponent.h"
|
||||
#import "RCTResizeMode.h"
|
||||
|
||||
@class RCTBridge;
|
||||
@class RCTImageSource;
|
||||
|
||||
@interface RCTImageView : UIImageView <RCTImageComponent>
|
||||
@interface RCTImageView : UIImageView
|
||||
|
||||
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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<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
|
||||
{
|
||||
if (_cachedTextStorage && width == _cachedTextStorageWidth && widthMode == _cachedTextStorageWidthMode) {
|
||||
|
@ -199,6 +238,11 @@ 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]]) {
|
||||
|
@ -213,19 +257,29 @@ static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t width
|
|||
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<RCTImageComponent>)child).image;
|
||||
imageAttachment.bounds = (CGRect){CGPointZero, {RCTZeroIfNaN(child.width), RCTZeroIfNaN(child.height)}};
|
||||
[attributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:imageAttachment]];
|
||||
} else {
|
||||
RCTLogError(@"<Text> can't have any children except <Text>, <Image> or raw strings");
|
||||
}
|
||||
|
||||
[child setTextComputed];
|
||||
} else {
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
[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
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <Text> tag. Not rendering string: '%@'",
|
||||
[(RCTShadowRawText *)shadowView text]);
|
||||
|
|
|
@ -217,7 +217,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -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
|
|
@ -140,12 +140,33 @@ typedef void (^RCTApplierBlock)(NSDictionary<NSNumber *, UIView *> *viewRegistry
|
|||
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
|
||||
viewsWithNewFrame:(NSMutableSet<RCTShadowView *> *)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<RCTShadowView *> *)viewsWithNewFrame
|
||||
absolutePosition:(CGPoint)absolutePosition;
|
||||
|
||||
/**
|
||||
* The following are implementation details exposed to subclasses. Do not call them directly
|
||||
*/
|
||||
|
|
|
@ -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<RCTShadowView *> *)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<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
|
||||
{
|
||||
CGPoint offset = CGPointZero;
|
||||
|
@ -459,6 +496,7 @@ RCT_BORDER_PROPERTY(Right, RIGHT)
|
|||
{ \
|
||||
_cssNode->style.dimensions[CSS_##cssProp] = value; \
|
||||
[self dirtyLayout]; \
|
||||
[self dirtyText]; \
|
||||
} \
|
||||
- (CGFloat)getProp \
|
||||
{ \
|
||||
|
|
14
docs/Text.md
14
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
|
||||
<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
|
||||
|
||||
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.
|
||||
|
|
Loading…
Reference in New Issue