mirror of
https://github.com/status-im/react-native.git
synced 2025-01-10 09:35:48 +00:00
37f4ec6e16
Summary: To prevent layout popping, when inserting images inside text we would render a blank placeholder image while the real image was loading. It turns out that this isn't necessary, as we can just specify the size of the image without having an actual image to display. Reviewed By: javache Differential Revision: D3212766 fb-gh-sync-id: e98851b32a2d0ae809fc0a4be47e6b77f3b17996 fbshipit-source-id: e98851b32a2d0ae809fc0a4be47e6b77f3b17996
420 lines
16 KiB
Objective-C
420 lines
16 KiB
Objective-C
/**
|
|
* 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 "RCTShadowText.h"
|
|
|
|
#import "RCTAccessibilityManager.h"
|
|
#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 RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName";
|
|
NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName";
|
|
|
|
@implementation RCTShadowText
|
|
{
|
|
NSTextStorage *_cachedTextStorage;
|
|
CGFloat _cachedTextStorageWidth;
|
|
CGFloat _cachedTextStorageWidthMode;
|
|
NSAttributedString *_cachedAttributedString;
|
|
CGFloat _effectiveLetterSpacing;
|
|
}
|
|
|
|
static css_dim_t RCTMeasure(void *context, float width, css_measure_mode_t widthMode, float height, css_measure_mode_t heightMode)
|
|
{
|
|
RCTShadowText *shadowText = (__bridge RCTShadowText *)context;
|
|
NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width widthMode:widthMode];
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
CGSize computedSize = [layoutManager usedRectForTextContainer:textContainer].size;
|
|
|
|
css_dim_t result;
|
|
result.dimensions[CSS_WIDTH] = RCTCeilPixelValue(computedSize.width);
|
|
if (shadowText->_effectiveLetterSpacing < 0) {
|
|
result.dimensions[CSS_WIDTH] -= shadowText->_effectiveLetterSpacing;
|
|
}
|
|
result.dimensions[CSS_HEIGHT] = RCTCeilPixelValue(computedSize.height);
|
|
return result;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
if ((self = [super init])) {
|
|
_fontSize = NAN;
|
|
_letterSpacing = NAN;
|
|
_isHighlighted = NO;
|
|
_textDecorationStyle = NSUnderlineStyleSingle;
|
|
_opacity = 1.0;
|
|
_cachedTextStorageWidth = -1;
|
|
_cachedTextStorageWidthMode = -1;
|
|
_fontSizeMultiplier = 1.0;
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(contentSizeMultiplierDidChange:)
|
|
name:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification
|
|
object:nil];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
NSString *superDescription = super.description;
|
|
return [[superDescription substringToIndex:superDescription.length - 1] stringByAppendingFormat:@"; text: %@>", [self attributedString].string];
|
|
}
|
|
|
|
- (void)contentSizeMultiplierDidChange:(NSNotification *)note
|
|
{
|
|
[self dirtyLayout];
|
|
[self dirtyText];
|
|
}
|
|
|
|
- (NSDictionary<NSString *, id> *)processUpdatedProperties:(NSMutableSet<RCTApplierBlock> *)applierBlocks
|
|
parentProperties:(NSDictionary<NSString *, id> *)parentProperties
|
|
{
|
|
if ([[self reactSuperview] isKindOfClass:[RCTShadowText class]]) {
|
|
return parentProperties;
|
|
}
|
|
|
|
parentProperties = [super processUpdatedProperties:applierBlocks
|
|
parentProperties:parentProperties];
|
|
|
|
UIEdgeInsets padding = self.paddingAsInsets;
|
|
CGFloat width = self.frame.size.width - (padding.left + padding.right);
|
|
|
|
NSTextStorage *textStorage = [self buildTextStorageForWidth:width widthMode:CSS_MEASURE_MODE_EXACTLY];
|
|
[applierBlocks addObject:^(NSDictionary<NSNumber *, RCTText *> *viewRegistry) {
|
|
RCTText *view = viewRegistry[self.reactTag];
|
|
view.textStorage = textStorage;
|
|
}];
|
|
|
|
return parentProperties;
|
|
}
|
|
|
|
- (void)applyLayoutNode:(css_node_t *)node
|
|
viewsWithNewFrame:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame
|
|
absolutePosition:(CGPoint)absolutePosition
|
|
{
|
|
[super applyLayoutNode:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition];
|
|
[self dirtyPropagation];
|
|
}
|
|
|
|
- (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode
|
|
{
|
|
if (_cachedTextStorage && width == _cachedTextStorageWidth && widthMode == _cachedTextStorageWidthMode) {
|
|
return _cachedTextStorage;
|
|
}
|
|
|
|
NSLayoutManager *layoutManager = [NSLayoutManager new];
|
|
|
|
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedString];
|
|
[textStorage addLayoutManager:layoutManager];
|
|
|
|
NSTextContainer *textContainer = [NSTextContainer new];
|
|
textContainer.lineFragmentPadding = 0.0;
|
|
textContainer.lineBreakMode = _numberOfLines > 0 ? NSLineBreakByTruncatingTail : NSLineBreakByClipping;
|
|
textContainer.maximumNumberOfLines = _numberOfLines;
|
|
textContainer.size = (CGSize){widthMode == CSS_MEASURE_MODE_UNDEFINED ? CGFLOAT_MAX : width, CGFLOAT_MAX};
|
|
|
|
[layoutManager addTextContainer:textContainer];
|
|
[layoutManager ensureLayoutForTextContainer:textContainer];
|
|
|
|
_cachedTextStorageWidth = width;
|
|
_cachedTextStorageWidthMode = widthMode;
|
|
_cachedTextStorage = textStorage;
|
|
|
|
return textStorage;
|
|
}
|
|
|
|
- (void)dirtyText
|
|
{
|
|
[super dirtyText];
|
|
_cachedTextStorage = nil;
|
|
}
|
|
|
|
- (void)recomputeText
|
|
{
|
|
[self attributedString];
|
|
[self setTextComputed];
|
|
[self dirtyPropagation];
|
|
}
|
|
|
|
- (NSAttributedString *)attributedString
|
|
{
|
|
return [self _attributedStringWithFontFamily:nil
|
|
fontSize:nil
|
|
fontWeight:nil
|
|
fontStyle:nil
|
|
letterSpacing:nil
|
|
useBackgroundColor:NO
|
|
foregroundColor:self.color ?: [UIColor blackColor]
|
|
backgroundColor:self.backgroundColor
|
|
opacity:self.opacity];
|
|
}
|
|
|
|
- (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily
|
|
fontSize:(NSNumber *)fontSize
|
|
fontWeight:(NSString *)fontWeight
|
|
fontStyle:(NSString *)fontStyle
|
|
letterSpacing:(NSNumber *)letterSpacing
|
|
useBackgroundColor:(BOOL)useBackgroundColor
|
|
foregroundColor:(UIColor *)foregroundColor
|
|
backgroundColor:(UIColor *)backgroundColor
|
|
opacity:(CGFloat)opacity
|
|
{
|
|
if (![self isTextDirty] && _cachedAttributedString) {
|
|
return _cachedAttributedString;
|
|
}
|
|
|
|
if (_fontSize && !isnan(_fontSize)) {
|
|
fontSize = @(_fontSize);
|
|
}
|
|
if (_fontWeight) {
|
|
fontWeight = _fontWeight;
|
|
}
|
|
if (_fontStyle) {
|
|
fontStyle = _fontStyle;
|
|
}
|
|
if (_fontFamily) {
|
|
fontFamily = _fontFamily;
|
|
}
|
|
if (!isnan(_letterSpacing)) {
|
|
letterSpacing = @(_letterSpacing);
|
|
}
|
|
|
|
_effectiveLetterSpacing = letterSpacing.doubleValue;
|
|
|
|
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]];
|
|
} 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];
|
|
}
|
|
|
|
[self _addAttribute:NSForegroundColorAttributeName
|
|
withValue:[foregroundColor colorWithAlphaComponent:CGColorGetAlpha(foregroundColor.CGColor) * opacity]
|
|
toAttributedString:attributedString];
|
|
|
|
if (_isHighlighted) {
|
|
[self _addAttribute:RCTIsHighlightedAttributeName withValue:@YES toAttributedString:attributedString];
|
|
}
|
|
if (useBackgroundColor && backgroundColor) {
|
|
[self _addAttribute:NSBackgroundColorAttributeName
|
|
withValue:[backgroundColor colorWithAlphaComponent:CGColorGetAlpha(backgroundColor.CGColor) * opacity]
|
|
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];
|
|
|
|
// create a non-mutable attributedString for use by the Text system which avoids copies down the line
|
|
_cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:attributedString];
|
|
[self dirtyLayout];
|
|
|
|
return _cachedAttributedString;
|
|
}
|
|
|
|
- (void)_addAttribute:(NSString *)attribute withValue:(id)attributeValue toAttributedString:(NSMutableAttributedString *)attributedString
|
|
{
|
|
[attributedString enumerateAttribute:attribute inRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
|
|
if (!value && attributeValue) {
|
|
[attributedString addAttribute:attribute value:attributeValue range:range];
|
|
}
|
|
}];
|
|
}
|
|
|
|
/*
|
|
* LineHeight works the same way line-height works in the web: if children and self have
|
|
* varying lineHeights, we simply take the max.
|
|
*/
|
|
- (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attributedString
|
|
{
|
|
// check if we have lineHeight set on self
|
|
__block BOOL hasParagraphStyle = NO;
|
|
if (_lineHeight || _textAlign) {
|
|
hasParagraphStyle = YES;
|
|
}
|
|
|
|
if (!_lineHeight) {
|
|
self.lineHeight = 0.0;
|
|
}
|
|
|
|
CGFloat fontSizeMultiplier = _allowFontScaling ? _fontSizeMultiplier : 1.0;
|
|
|
|
// check for lineHeight on each of our children, update the max as we go (in self.lineHeight)
|
|
[attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:(NSRange){0, attributedString.length} options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
|
|
if (value) {
|
|
NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value;
|
|
CGFloat maximumLineHeight = round(paragraphStyle.maximumLineHeight / fontSizeMultiplier);
|
|
if (maximumLineHeight > self.lineHeight) {
|
|
self.lineHeight = maximumLineHeight;
|
|
}
|
|
hasParagraphStyle = YES;
|
|
}
|
|
}];
|
|
|
|
self.textAlign = _textAlign ?: NSTextAlignmentNatural;
|
|
self.writingDirection = _writingDirection ?: NSWritingDirectionNatural;
|
|
|
|
// if we found anything, set it :D
|
|
if (hasParagraphStyle) {
|
|
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
|
|
paragraphStyle.alignment = _textAlign;
|
|
paragraphStyle.baseWritingDirection = _writingDirection;
|
|
CGFloat lineHeight = round(_lineHeight * fontSizeMultiplier);
|
|
paragraphStyle.minimumLineHeight = lineHeight;
|
|
paragraphStyle.maximumLineHeight = lineHeight;
|
|
[attributedString addAttribute:NSParagraphStyleAttributeName
|
|
value:paragraphStyle
|
|
range:(NSRange){0, attributedString.length}];
|
|
}
|
|
|
|
// Text decoration
|
|
if (_textDecorationLine == RCTTextDecorationLineTypeUnderline ||
|
|
_textDecorationLine == RCTTextDecorationLineTypeUnderlineStrikethrough) {
|
|
[self _addAttribute:NSUnderlineStyleAttributeName withValue:@(_textDecorationStyle)
|
|
toAttributedString:attributedString];
|
|
}
|
|
if (_textDecorationLine == RCTTextDecorationLineTypeStrikethrough ||
|
|
_textDecorationLine == RCTTextDecorationLineTypeUnderlineStrikethrough){
|
|
[self _addAttribute:NSStrikethroughStyleAttributeName withValue:@(_textDecorationStyle)
|
|
toAttributedString:attributedString];
|
|
}
|
|
if (_textDecorationColor) {
|
|
[self _addAttribute:NSStrikethroughColorAttributeName withValue:_textDecorationColor
|
|
toAttributedString:attributedString];
|
|
[self _addAttribute:NSUnderlineColorAttributeName withValue:_textDecorationColor
|
|
toAttributedString:attributedString];
|
|
}
|
|
|
|
// Text shadow
|
|
if (!CGSizeEqualToSize(_textShadowOffset, CGSizeZero)) {
|
|
NSShadow *shadow = [NSShadow new];
|
|
shadow.shadowOffset = _textShadowOffset;
|
|
shadow.shadowBlurRadius = _textShadowRadius;
|
|
shadow.shadowColor = _textShadowColor;
|
|
[self _addAttribute:NSShadowAttributeName withValue:shadow toAttributedString:attributedString];
|
|
}
|
|
}
|
|
|
|
- (void)fillCSSNode:(css_node_t *)node
|
|
{
|
|
[super fillCSSNode:node];
|
|
node->measure = RCTMeasure;
|
|
node->children_count = 0;
|
|
}
|
|
|
|
- (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex
|
|
{
|
|
[super insertReactSubview:subview atIndex:atIndex];
|
|
self.cssNode->children_count = 0;
|
|
}
|
|
|
|
- (void)removeReactSubview:(RCTShadowView *)subview
|
|
{
|
|
[super removeReactSubview:subview];
|
|
self.cssNode->children_count = 0;
|
|
}
|
|
|
|
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
|
{
|
|
super.backgroundColor = backgroundColor;
|
|
[self dirtyText];
|
|
}
|
|
|
|
#define RCT_TEXT_PROPERTY(setProp, ivar, type) \
|
|
- (void)set##setProp:(type)value; \
|
|
{ \
|
|
ivar = value; \
|
|
[self dirtyText]; \
|
|
}
|
|
|
|
RCT_TEXT_PROPERTY(Color, _color, UIColor *)
|
|
RCT_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *)
|
|
RCT_TEXT_PROPERTY(FontSize, _fontSize, CGFloat)
|
|
RCT_TEXT_PROPERTY(FontWeight, _fontWeight, NSString *)
|
|
RCT_TEXT_PROPERTY(FontStyle, _fontStyle, NSString *)
|
|
RCT_TEXT_PROPERTY(IsHighlighted, _isHighlighted, BOOL)
|
|
RCT_TEXT_PROPERTY(LetterSpacing, _letterSpacing, CGFloat)
|
|
RCT_TEXT_PROPERTY(LineHeight, _lineHeight, CGFloat)
|
|
RCT_TEXT_PROPERTY(NumberOfLines, _numberOfLines, NSUInteger)
|
|
RCT_TEXT_PROPERTY(TextAlign, _textAlign, NSTextAlignment)
|
|
RCT_TEXT_PROPERTY(TextDecorationColor, _textDecorationColor, UIColor *);
|
|
RCT_TEXT_PROPERTY(TextDecorationLine, _textDecorationLine, RCTTextDecorationLineType);
|
|
RCT_TEXT_PROPERTY(TextDecorationStyle, _textDecorationStyle, NSUnderlineStyle);
|
|
RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection)
|
|
RCT_TEXT_PROPERTY(Opacity, _opacity, CGFloat)
|
|
RCT_TEXT_PROPERTY(TextShadowOffset, _textShadowOffset, CGSize);
|
|
RCT_TEXT_PROPERTY(TextShadowRadius, _textShadowRadius, CGFloat);
|
|
RCT_TEXT_PROPERTY(TextShadowColor, _textShadowColor, UIColor *);
|
|
|
|
- (void)setAllowFontScaling:(BOOL)allowFontScaling
|
|
{
|
|
_allowFontScaling = allowFontScaling;
|
|
for (RCTShadowView *child in [self reactSubviews]) {
|
|
if ([child isKindOfClass:[RCTShadowText class]]) {
|
|
((RCTShadowText *)child).allowFontScaling = allowFontScaling;
|
|
}
|
|
}
|
|
[self dirtyText];
|
|
}
|
|
|
|
- (void)setFontSizeMultiplier:(CGFloat)fontSizeMultiplier
|
|
{
|
|
_fontSizeMultiplier = fontSizeMultiplier;
|
|
if (_fontSizeMultiplier == 0) {
|
|
RCTLogError(@"fontSizeMultiplier value must be > zero.");
|
|
_fontSizeMultiplier = 1.0;
|
|
}
|
|
for (RCTShadowView *child in [self reactSubviews]) {
|
|
if ([child isKindOfClass:[RCTShadowText class]]) {
|
|
((RCTShadowText *)child).fontSizeMultiplier = fontSizeMultiplier;
|
|
}
|
|
}
|
|
[self dirtyText];
|
|
}
|
|
|
|
@end
|