693 lines
27 KiB
Objective-C
693 lines
27 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 <React/RCTAccessibilityManager.h>
|
|
#import <React/RCTBridge.h>
|
|
#import <React/RCTConvert.h>
|
|
#import <React/RCTFont.h>
|
|
#import <React/RCTLog.h>
|
|
#import <React/RCTShadowView+Layout.h>
|
|
#import <React/RCTUIManager.h>
|
|
#import <React/RCTUtils.h>
|
|
|
|
#import "RCTShadowRawText.h"
|
|
#import "RCTText.h"
|
|
#import "RCTTextView.h"
|
|
|
|
NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName";
|
|
NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName";
|
|
|
|
static NSString *const kShadowViewAttributeName = @"RCTShadowViewAttributeName";
|
|
|
|
static CGFloat const kAutoSizeWidthErrorMargin = 0.05f;
|
|
static CGFloat const kAutoSizeHeightErrorMargin = 0.025f;
|
|
static CGFloat const kAutoSizeGranularity = 0.001f;
|
|
|
|
@implementation RCTShadowText
|
|
{
|
|
NSTextStorage *_cachedTextStorage;
|
|
CGFloat _cachedTextStorageWidth;
|
|
CGFloat _cachedTextStorageWidthMode;
|
|
NSAttributedString *_cachedAttributedString;
|
|
CGFloat _effectiveLetterSpacing;
|
|
UIUserInterfaceLayoutDirection _cachedEffectiveLayoutDirection;
|
|
}
|
|
|
|
static YGSize RCTMeasure(YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
|
|
{
|
|
RCTShadowText *shadowText = (__bridge RCTShadowText *)YGNodeGetContext(node);
|
|
NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width widthMode:widthMode];
|
|
[shadowText calculateTextFrame:textStorage];
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
CGSize computedSize = [layoutManager usedRectForTextContainer:textContainer].size;
|
|
|
|
YGSize result;
|
|
result.width = RCTCeilPixelValue(computedSize.width);
|
|
if (shadowText->_effectiveLetterSpacing < 0) {
|
|
result.width -= shadowText->_effectiveLetterSpacing;
|
|
}
|
|
result.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;
|
|
_textAlign = NSTextAlignmentNatural;
|
|
_writingDirection = NSWritingDirectionNatural;
|
|
_cachedEffectiveLayoutDirection = UIUserInterfaceLayoutDirectionLeftToRight;
|
|
|
|
YGNodeSetMeasureFunc(self.yogaNode, RCTMeasure);
|
|
|
|
[[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];
|
|
}
|
|
|
|
- (BOOL)isYogaLeafNode
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (void)contentSizeMultiplierDidChange:(NSNotification *)note
|
|
{
|
|
YGNodeMarkDirty(self.yogaNode);
|
|
[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];
|
|
|
|
CGFloat availableWidth = self.availableSize.width;
|
|
NSNumber *parentTag = [[self reactSuperview] reactTag];
|
|
NSTextStorage *textStorage = [self buildTextStorageForWidth:availableWidth widthMode:YGMeasureModeExactly];
|
|
CGRect textFrame = [self calculateTextFrame:textStorage];
|
|
BOOL selectable = _selectable;
|
|
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
RCTText *view = (RCTText *)viewRegistry[self.reactTag];
|
|
view.textFrame = textFrame;
|
|
view.textStorage = textStorage;
|
|
view.selectable = selectable;
|
|
|
|
/**
|
|
* NOTE: this logic is included to support rich text editing inside multiline
|
|
* `<TextInput>` controls. It is required in order to ensure that the
|
|
* textStorage (aka attributed string) is copied over from the RCTShadowText
|
|
* to the RCTText view in time to be used to update the editable text content.
|
|
* TODO: we should establish a delegate relationship betweeen RCTTextView
|
|
* and its contaned RCTText element when they get inserted and get rid of this
|
|
*/
|
|
UIView *parentView = viewRegistry[parentTag];
|
|
if ([parentView respondsToSelector:@selector(performTextUpdate)]) {
|
|
[(RCTTextView *)parentView performTextUpdate];
|
|
}
|
|
}];
|
|
|
|
return parentProperties;
|
|
}
|
|
|
|
- (void)applyLayoutNode:(YGNodeRef)node
|
|
viewsWithNewFrame:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame
|
|
absolutePosition:(CGPoint)absolutePosition
|
|
{
|
|
[super applyLayoutNode:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition];
|
|
[self dirtyPropagation];
|
|
}
|
|
|
|
- (void)applyLayoutToChildren:(YGNodeRef)node
|
|
viewsWithNewFrame:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame
|
|
absolutePosition:(CGPoint)absolutePosition
|
|
{
|
|
// Run layout on subviews.
|
|
CGFloat availableWidth = self.availableSize.width;
|
|
NSTextStorage *textStorage = [self buildTextStorageForWidth:availableWidth widthMode:YGMeasureModeExactly];
|
|
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:kShadowViewAttributeName inRange:characterRange options:0 usingBlock:^(RCTShadowView *child, NSRange range, BOOL *_) {
|
|
if (child) {
|
|
YGNodeRef childNode = child.yogaNode;
|
|
float width = YGNodeStyleGetWidth(childNode).value;
|
|
float height = YGNodeStyleGetHeight(childNode).value;
|
|
if (YGFloatIsUndefined(width) || YGFloatIsUndefined(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:(YGMeasureMode)widthMode
|
|
{
|
|
if (
|
|
_cachedTextStorage &&
|
|
(width == _cachedTextStorageWidth || (isnan(width) && isnan(_cachedTextStorageWidth))) &&
|
|
widthMode == _cachedTextStorageWidthMode &&
|
|
_cachedEffectiveLayoutDirection == self.effectiveLayoutDirection
|
|
) {
|
|
return _cachedTextStorage;
|
|
}
|
|
|
|
NSLayoutManager *layoutManager = [NSLayoutManager new];
|
|
|
|
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedString];
|
|
[textStorage addLayoutManager:layoutManager];
|
|
|
|
NSTextContainer *textContainer = [NSTextContainer new];
|
|
textContainer.lineFragmentPadding = 0.0;
|
|
|
|
if (_numberOfLines > 0) {
|
|
textContainer.lineBreakMode = _ellipsizeMode;
|
|
} else {
|
|
textContainer.lineBreakMode = NSLineBreakByClipping;
|
|
}
|
|
|
|
textContainer.maximumNumberOfLines = _numberOfLines;
|
|
textContainer.size = (CGSize){
|
|
widthMode == YGMeasureModeUndefined || isnan(width) ? 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 &&
|
|
_cachedEffectiveLayoutDirection == self.effectiveLayoutDirection
|
|
) {
|
|
return _cachedAttributedString;
|
|
}
|
|
|
|
_cachedEffectiveLayoutDirection = self.effectiveLayoutDirection;
|
|
|
|
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;
|
|
|
|
UIFont *font = [RCTFont updateFont:nil
|
|
withFamily:fontFamily
|
|
size:fontSize
|
|
weight:fontWeight
|
|
style:fontStyle
|
|
variant:_fontVariant
|
|
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]];
|
|
[child setTextComputed];
|
|
} else if ([child isKindOfClass:[RCTShadowRawText class]]) {
|
|
RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child;
|
|
[attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:shadowRawText.text ?: @""]];
|
|
[child setTextComputed];
|
|
} else {
|
|
float width = YGNodeStyleGetWidth(child.yogaNode).value;
|
|
float height = YGNodeStyleGetHeight(child.yogaNode).value;
|
|
if (YGFloatIsUndefined(width) || YGFloatIsUndefined(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:kShadowViewAttributeName 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
|
|
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];
|
|
}
|
|
|
|
[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
|
|
fontLineHeight:font.lineHeight
|
|
heightOfTallestSubview:heightOfTallestSubview];
|
|
|
|
// create a non-mutable attributedString for use by the Text system which avoids copies down the line
|
|
_cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:attributedString];
|
|
YGNodeMarkDirty(self.yogaNode);
|
|
|
|
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
|
|
fontLineHeight:(CGFloat)fontLineHeight
|
|
heightOfTallestSubview:(CGFloat)heightOfTallestSubview
|
|
{
|
|
__block BOOL hasParagraphStyle = NO;
|
|
|
|
if (_lineHeight != 0.0 || _textAlign != NSTextAlignmentNatural) {
|
|
hasParagraphStyle = YES;
|
|
}
|
|
|
|
CGFloat fontSizeMultiplier = _allowFontScaling ? _fontSizeMultiplier : 1.0;
|
|
|
|
// Text line height
|
|
__block float compoundLineHeight = _lineHeight * fontSizeMultiplier;
|
|
|
|
// Checking for `maximumLineHeight` on each of our children and updating `compoundLineHeight` with the maximum value on the go.
|
|
[attributedString enumerateAttribute:NSParagraphStyleAttributeName
|
|
inRange:(NSRange){0, attributedString.length}
|
|
options:0
|
|
usingBlock:^(NSParagraphStyle *paragraphStyle, NSRange range, BOOL *stop) {
|
|
|
|
if (!paragraphStyle) {
|
|
return;
|
|
}
|
|
|
|
hasParagraphStyle = YES;
|
|
compoundLineHeight = MAX(compoundLineHeight, paragraphStyle.maximumLineHeight);
|
|
}];
|
|
|
|
compoundLineHeight = MAX(round(compoundLineHeight), ceilf(heightOfTallestSubview));
|
|
|
|
// Text alignment
|
|
NSTextAlignment textAlign = _textAlign;
|
|
if (textAlign == NSTextAlignmentRight || textAlign == NSTextAlignmentLeft) {
|
|
if (_cachedEffectiveLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
|
|
if (textAlign == NSTextAlignmentRight) {
|
|
textAlign = NSTextAlignmentLeft;
|
|
} else {
|
|
textAlign = NSTextAlignmentRight;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasParagraphStyle) {
|
|
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
|
|
paragraphStyle.alignment = textAlign;
|
|
paragraphStyle.baseWritingDirection = _writingDirection;
|
|
paragraphStyle.minimumLineHeight = compoundLineHeight;
|
|
paragraphStyle.maximumLineHeight = compoundLineHeight;
|
|
[attributedString addAttribute:NSParagraphStyleAttributeName
|
|
value:paragraphStyle
|
|
range:(NSRange){0, attributedString.length}];
|
|
|
|
if (compoundLineHeight > fontLineHeight) {
|
|
[attributedString addAttribute:NSBaselineOffsetAttributeName
|
|
value:@(compoundLineHeight / 2 - fontLineHeight / 2)
|
|
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];
|
|
}
|
|
}
|
|
|
|
#pragma mark Autosizing
|
|
|
|
- (CGRect)calculateTextFrame:(NSTextStorage *)textStorage
|
|
{
|
|
CGRect textFrame = UIEdgeInsetsInsetRect((CGRect){CGPointZero, self.frame.size},
|
|
self.compoundInsets);
|
|
|
|
if (_adjustsFontSizeToFit) {
|
|
textFrame = [self updateStorage:textStorage toFitFrame:textFrame];
|
|
}
|
|
|
|
return textFrame;
|
|
}
|
|
|
|
- (CGRect)updateStorage:(NSTextStorage *)textStorage toFitFrame:(CGRect)frame
|
|
{
|
|
BOOL fits = [self attemptScale:1.0f
|
|
inStorage:textStorage
|
|
forFrame:frame];
|
|
CGSize requiredSize;
|
|
if (!fits) {
|
|
requiredSize = [self calculateOptimumScaleInFrame:frame
|
|
forStorage:textStorage
|
|
minScale:self.minimumFontScale
|
|
maxScale:1.0
|
|
prevMid:INT_MAX];
|
|
} else {
|
|
requiredSize = [self calculateSize:textStorage];
|
|
}
|
|
|
|
// Vertically center draw position for new text sizing.
|
|
frame.origin.y = self.compoundInsets.top + RCTRoundPixelValue((CGRectGetHeight(frame) - requiredSize.height) / 2.0f);
|
|
return frame;
|
|
}
|
|
|
|
- (CGSize)calculateOptimumScaleInFrame:(CGRect)frame
|
|
forStorage:(NSTextStorage *)textStorage
|
|
minScale:(CGFloat)minScale
|
|
maxScale:(CGFloat)maxScale
|
|
prevMid:(CGFloat)prevMid
|
|
{
|
|
CGFloat midScale = (minScale + maxScale) / 2.0f;
|
|
if (round((prevMid / kAutoSizeGranularity)) == round((midScale / kAutoSizeGranularity))) {
|
|
//Bail because we can't meet error margin.
|
|
return [self calculateSize:textStorage];
|
|
} else {
|
|
RCTSizeComparison comparison = [self attemptScale:midScale
|
|
inStorage:textStorage
|
|
forFrame:frame];
|
|
if (comparison == RCTSizeWithinRange) {
|
|
return [self calculateSize:textStorage];
|
|
} else if (comparison == RCTSizeTooLarge) {
|
|
return [self calculateOptimumScaleInFrame:frame
|
|
forStorage:textStorage
|
|
minScale:minScale
|
|
maxScale:midScale - kAutoSizeGranularity
|
|
prevMid:midScale];
|
|
} else {
|
|
return [self calculateOptimumScaleInFrame:frame
|
|
forStorage:textStorage
|
|
minScale:midScale + kAutoSizeGranularity
|
|
maxScale:maxScale
|
|
prevMid:midScale];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (RCTSizeComparison)attemptScale:(CGFloat)scale
|
|
inStorage:(NSTextStorage *)textStorage
|
|
forFrame:(CGRect)frame
|
|
{
|
|
NSLayoutManager *layoutManager = [textStorage.layoutManagers firstObject];
|
|
NSTextContainer *textContainer = [layoutManager.textContainers firstObject];
|
|
|
|
NSRange glyphRange = NSMakeRange(0, textStorage.length);
|
|
[textStorage beginEditing];
|
|
[textStorage enumerateAttribute:NSFontAttributeName
|
|
inRange:glyphRange
|
|
options:0
|
|
usingBlock:^(UIFont *font, NSRange range, BOOL *stop)
|
|
{
|
|
if (font) {
|
|
UIFont *originalFont = [self.attributedString attribute:NSFontAttributeName
|
|
atIndex:range.location
|
|
effectiveRange:&range];
|
|
UIFont *newFont = [font fontWithSize:originalFont.pointSize * scale];
|
|
[textStorage removeAttribute:NSFontAttributeName range:range];
|
|
[textStorage addAttribute:NSFontAttributeName value:newFont range:range];
|
|
}
|
|
}];
|
|
|
|
[textStorage endEditing];
|
|
|
|
NSInteger linesRequired = [self numberOfLinesRequired:[textStorage.layoutManagers firstObject]];
|
|
CGSize requiredSize = [self calculateSize:textStorage];
|
|
|
|
BOOL fitSize = requiredSize.height <= CGRectGetHeight(frame) &&
|
|
requiredSize.width <= CGRectGetWidth(frame);
|
|
|
|
BOOL fitLines = linesRequired <= textContainer.maximumNumberOfLines ||
|
|
textContainer.maximumNumberOfLines == 0;
|
|
|
|
if (fitLines && fitSize) {
|
|
if ((requiredSize.width + (CGRectGetWidth(frame) * kAutoSizeWidthErrorMargin)) > CGRectGetWidth(frame) &&
|
|
(requiredSize.height + (CGRectGetHeight(frame) * kAutoSizeHeightErrorMargin)) > CGRectGetHeight(frame))
|
|
{
|
|
return RCTSizeWithinRange;
|
|
} else {
|
|
return RCTSizeTooSmall;
|
|
}
|
|
} else {
|
|
return RCTSizeTooLarge;
|
|
}
|
|
}
|
|
|
|
// Via Apple Text Layout Programming Guide
|
|
// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextLayout/Tasks/CountLines.html
|
|
- (NSInteger)numberOfLinesRequired:(NSLayoutManager *)layoutManager
|
|
{
|
|
NSInteger numberOfLines, index, numberOfGlyphs = [layoutManager numberOfGlyphs];
|
|
NSRange lineRange;
|
|
for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
|
|
(void) [layoutManager lineFragmentRectForGlyphAtIndex:index
|
|
effectiveRange:&lineRange];
|
|
index = NSMaxRange(lineRange);
|
|
}
|
|
|
|
return numberOfLines;
|
|
}
|
|
|
|
// Via Apple Text Layout Programming Guide
|
|
//https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextLayout/Tasks/StringHeight.html
|
|
- (CGSize)calculateSize:(NSTextStorage *)storage
|
|
{
|
|
NSLayoutManager *layoutManager = [storage.layoutManagers firstObject];
|
|
NSTextContainer *textContainer = [layoutManager.textContainers firstObject];
|
|
|
|
[textContainer setLineBreakMode:NSLineBreakByWordWrapping];
|
|
NSInteger maxLines = [textContainer maximumNumberOfLines];
|
|
[textContainer setMaximumNumberOfLines:0];
|
|
(void) [layoutManager glyphRangeForTextContainer:textContainer];
|
|
CGSize requiredSize = [layoutManager usedRectForTextContainer:textContainer].size;
|
|
[textContainer setMaximumNumberOfLines:maxLines];
|
|
|
|
return requiredSize;
|
|
}
|
|
|
|
- (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(AdjustsFontSizeToFit, _adjustsFontSizeToFit, BOOL)
|
|
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(FontVariant, _fontVariant, NSArray *)
|
|
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(EllipsizeMode, _ellipsizeMode, NSLineBreakMode)
|
|
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];
|
|
}
|
|
|
|
- (void)setMinimumFontScale:(CGFloat)minimumFontScale
|
|
{
|
|
if (minimumFontScale >= 0.01) {
|
|
_minimumFontScale = minimumFontScale;
|
|
}
|
|
[self dirtyText];
|
|
}
|
|
|
|
@end
|