react-native/Libraries/Text/RCTShadowText.m
Nick Lockwood e4c53c28ae Improved shadow performance
Summary:
public
React Native currently exposes the iOS layer shadow properties more-or-less directly, however there are a number of problems with this:

1) Performance when using these properties is poor by default. That's because iOS calculates the shadow by getting the exact pixel mask of the view, including any tranlucent content, and all of its subviews, which is very CPU and GPU-intensive.
2) The iOS shadow properties do not match the syntax or semantics of the CSS box-shadow standard, and are unlikely to be possible to implement on Android.
3) We don't expose the `layer.shadowPath` property, which is crucial to getting good performance out of layer shadows.

This diff solves problem number 1) by implementing a default `shadowPath` that matches the view border for views with an opaque background. This improves the performance of shadows by optimizing for the common usage case. I've also reinstated background color propagation for views which have shadow props - this should help ensure that this best-case scenario occurs more often.

For views with an explicit transparent background, the shadow will continue to work as it did before ( `shadowPath` will be left unset, and the shadow will be derived exactly from the pixels of the view and its subviews). This is the worst-case path for performance, however, so you should avoid it unless absolutely necessary. **Support for this may be disabled by default in future, or dropped altogether.**

For translucent images, it is suggested that you bake the shadow into the image itself, or use another mechanism to pre-generate the shadow. For text shadows, you should use the textShadow properties, which work cross-platform and have much better performance.

Problem number 2) will be solved in a future diff, possibly by renaming the iOS shadowXXX properties to boxShadowXXX, and changing the syntax and semantics to match the CSS standards.

Problem number 3) is now mostly moot, since we generate the shadowPath automatically. In future, we may provide an iOS-specific prop to set the path explicitly if there's a demand for more precise control of the shadow.

Reviewed By: weicool

Differential Revision: D2827581

fb-gh-sync-id: 853aa018e1d61d5f88304c6fc1b78f9d7e739804
2016-01-14 14:04:34 -08:00

409 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;
NSAttributedString *_cachedAttributedString;
CGFloat _effectiveLetterSpacing;
}
static css_dim_t RCTMeasure(void *context, float width, float height)
{
RCTShadowText *shadowText = (__bridge RCTShadowText *)context;
NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width];
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;
[[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
{
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];
[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
{
if (_cachedTextStorage && width == _cachedTextStorageWidth) {
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){isnan(width) ? CGFLOAT_MAX : width, CGFLOAT_MAX};
[layoutManager addTextContainer:textContainer];
[layoutManager ensureLayoutForTextContainer:textContainer];
_cachedTextStorageWidth = width;
_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)]) {
UIImage *image = ((id<RCTImageComponent>)child).image;
if (image) {
NSTextAttachment *imageAttachment = [NSTextAttachment new];
imageAttachment.image = image;
[attributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:imageAttachment]];
} else {
//TODO: add placeholder image?
}
} 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 > 0.0 ? _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;
}
// 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 / self.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 * (_allowFontScaling && self.fontSizeMultiplier > 0.0 ? self.fontSizeMultiplier : 1.0));
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;
for (RCTShadowView *child in [self reactSubviews]) {
if ([child isKindOfClass:[RCTShadowText class]]) {
((RCTShadowText *)child).fontSizeMultiplier = fontSizeMultiplier;
}
}
[self dirtyText];
}
@end