mirror of
https://github.com/Tencent/Hippy.git
synced 2026-10-28 13:38:10 +08:00
1016 lines
47 KiB
Plaintext
1016 lines
47 KiB
Plaintext
/*!
|
|
* iOS SDK
|
|
*
|
|
* Tencent is pleased to support the open source community by making
|
|
* Hippy available.
|
|
*
|
|
* Copyright (C) 2019 THL A29 Limited, a Tencent company.
|
|
* All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import "HippyShadowText.h"
|
|
#import "x5LayoutUtil.h"
|
|
#import "HippyBridge.h"
|
|
#import "HippyConvert.h"
|
|
#import "HippyFont.h"
|
|
#import "HippyLog.h"
|
|
#import "HippyText.h"
|
|
#import "HippyTextView.h"
|
|
#import "HippyUIManager.h"
|
|
#import "HippyUtils.h"
|
|
#import "HippyVirtualTextNode.h"
|
|
#import "HippyI18nUtils.h"
|
|
#import "HippyShadowView+MTTLayout.h"
|
|
|
|
|
|
// Text Attachment use this key to ref shadow view, HippyShadowView value
|
|
NSAttributedStringKey const HippyShadowViewAttributeName = @"HippyShadowViewAttributeName";
|
|
// Highlighted or not
|
|
NSAttributedStringKey const HippyIsHighlightedAttributeName = @"IsHighlightedAttributeName";
|
|
// Hippy Tag Key
|
|
NSAttributedStringKey const HippyHippyTagAttributeName = @"HippyTagAttributeName";
|
|
// VerticalAlign of Text or nested Text, NSNumber value
|
|
NSAttributedStringKey const HippyTextVerticalAlignAttributeName = @"HippyTextVerticalAlignAttributeName";
|
|
// Distance to the bottom of the baseline, for text attachment baseline layout, NSNumber value
|
|
NSAttributedStringKey const HippyVerticalAlignBaselineOffsetAttributeName = @"HippyVerticalAlignBaselineOffsetAttributeName";
|
|
|
|
|
|
CGFloat const HippyTextAutoSizeWidthErrorMargin = 0.05f;
|
|
CGFloat const HippyTextAutoSizeHeightErrorMargin = 0.025f;
|
|
CGFloat const HippyTextAutoSizeGranularity = 0.001f;
|
|
static const CGFloat gDefaultFontSize = 14.f;
|
|
|
|
|
|
#pragma mark - HippyAttributedStringStyleInfo
|
|
|
|
@implementation HippyAttributedStringStyleInfo
|
|
|
|
@end
|
|
|
|
|
|
#pragma mark - HippyShadowText
|
|
|
|
@interface HippyShadowText () <NSLayoutManagerDelegate>
|
|
{
|
|
BOOL _isNestedText; // Indicates whether Text is nested, for speeding up typesetting calculations
|
|
BOOL _needRelayoutText; // special styles require two layouts, eg. verticalAlign etc
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation HippyShadowText
|
|
// MTTlayout
|
|
static MTTSize x5MeasureFunc(
|
|
MTTNodeRef node, float width, MeasureMode widthMeasureMode, __unused float height, __unused MeasureMode heightMeasureMode, void *layoutContext) {
|
|
HippyShadowText *shadowText = (__bridge HippyShadowText *)MTTNodeGetContext(node);
|
|
NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width widthMode:widthMeasureMode];
|
|
[shadowText calculateTextFrame:textStorage];
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
CGSize computedSize = [layoutManager usedRectForTextContainer:textContainer].size;
|
|
|
|
MTTSize result;
|
|
result.width = x5CeilPixelValue(computedSize.width);
|
|
if (shadowText->_effectiveLetterSpacing < 0) {
|
|
result.width -= shadowText->_effectiveLetterSpacing;
|
|
}
|
|
result.height = x5CeilPixelValue(computedSize.height);
|
|
return result;
|
|
}
|
|
|
|
static void resetFontAttribute(NSTextStorage *textStorage) {
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSUInteger numberOfGlyphs = [layoutManager numberOfGlyphs];
|
|
if (0 != numberOfGlyphs) {
|
|
NSUInteger stringLength = [textStorage length];
|
|
NSRange lineFramentEffectiveRange = { 0, 0 };
|
|
[layoutManager lineFragmentUsedRectForGlyphAtIndex:numberOfGlyphs - 1 effectiveRange:&lineFramentEffectiveRange];
|
|
while (1 == lineFramentEffectiveRange.length) {
|
|
NSUInteger attributeIndex = 0;
|
|
NSRange shrinkEffectiveRange = { 0, 0 };
|
|
BOOL didShrinkKernSpacing = NO;
|
|
do {
|
|
NSNumber *kernValue = [textStorage attribute:NSKernAttributeName atIndex:attributeIndex effectiveRange:&shrinkEffectiveRange];
|
|
if (nil != (id)kernValue) {
|
|
NSNumber *previousKernValue = @([kernValue integerValue] - 1);
|
|
[textStorage addAttribute:NSKernAttributeName value:previousKernValue range:shrinkEffectiveRange];
|
|
didShrinkKernSpacing = YES;
|
|
}
|
|
attributeIndex += shrinkEffectiveRange.length;
|
|
} while (attributeIndex < stringLength);
|
|
|
|
if (NO == didShrinkKernSpacing) {
|
|
attributeIndex = 0;
|
|
shrinkEffectiveRange = { 0, 0 };
|
|
do {
|
|
UIFont *fontValue = [textStorage attribute:NSFontAttributeName atIndex:attributeIndex effectiveRange:&shrinkEffectiveRange];
|
|
if (nil != fontValue) {
|
|
CGFloat fontSize = [fontValue pointSize];
|
|
UIFont *smallerFont = [fontValue fontWithSize:fontSize - 1];
|
|
[textStorage addAttribute:NSFontAttributeName value:smallerFont range:shrinkEffectiveRange];
|
|
}
|
|
attributeIndex += shrinkEffectiveRange.length;
|
|
} while (attributeIndex < stringLength);
|
|
}
|
|
[layoutManager lineFragmentUsedRectForGlyphAtIndex:numberOfGlyphs - 1 effectiveRange:&lineFramentEffectiveRange];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (instancetype)init {
|
|
if ((self = [super init])) {
|
|
_fontSize = NAN;
|
|
_letterSpacing = NAN;
|
|
_isHighlighted = NO;
|
|
_textDecorationStyle = NSUnderlineStyleSingle;
|
|
_opacity = 1.0;
|
|
_ellipsizeMode = NSLineBreakByTruncatingTail;
|
|
_cachedTextStorageWidth = -1;
|
|
_cachedTextStorageWidthMode = -1;
|
|
_fontSizeMultiplier = 1.0;
|
|
_textAlign = NSTextAlignmentLeft;
|
|
if (NSWritingDirectionRightToLeft == [[HippyI18nUtils sharedInstance] writingDirectionForCurrentAppLanguage]) {
|
|
self.textAlign = NSTextAlignmentRight;
|
|
}
|
|
// MTTlayout
|
|
MTTNodeSetMeasureFunc(self.nodeRef, x5MeasureFunc);
|
|
MTTNodeSetContext(self.nodeRef, (__bridge void *)self);
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentSizeMultiplierDidChange:)
|
|
name:HippyUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification
|
|
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)isCSSLeafNode {
|
|
return YES;
|
|
}
|
|
|
|
- (void)contentSizeMultiplierDidChange:(__unused NSNotification *)note {
|
|
// MTTlayout
|
|
MTTNodeMarkDirty(self.nodeRef);
|
|
[self dirtyText];
|
|
}
|
|
|
|
- (NSDictionary<NSString *, id> *)processUpdatedProperties:(NSMutableSet<HippyApplierBlock> *)applierBlocks
|
|
parentProperties:(NSDictionary<NSString *, id> *)parentProperties {
|
|
if ([[self hippySuperview] isKindOfClass:[HippyShadowText class]]) {
|
|
return parentProperties;
|
|
}
|
|
|
|
parentProperties = [super processUpdatedProperties:applierBlocks parentProperties:parentProperties];
|
|
|
|
UIEdgeInsets padding = self.paddingAsInsets;
|
|
CGFloat width = self.frame.size.width - (padding.left + padding.right);
|
|
|
|
NSNumber *parentTag = [[self hippySuperview] hippyTag];
|
|
// MTTlayout
|
|
NSTextStorage *textStorage = [self buildTextStorageForWidth:width widthMode:MeasureModeExactly];
|
|
CGRect textFrame = [self calculateTextFrame:textStorage];
|
|
UIColor *color = self.color ?: [UIColor blackColor];
|
|
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
HippyText *view = (HippyText *)viewRegistry[self.hippyTag];
|
|
view.textFrame = textFrame;
|
|
view.textStorage = textStorage;
|
|
view.textColor = color;
|
|
/**
|
|
* 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 HippyShadowText
|
|
* to the HippyText view in time to be used to update the editable text content.
|
|
* TODO: we should establish a delegate relationship betweeen HippyTextView
|
|
* and its contaned HippyText element when they get inserted and get rid of this
|
|
*/
|
|
UIView *parentView = viewRegistry[parentTag];
|
|
if ([parentView respondsToSelector:@selector(performTextUpdate)]) {
|
|
[(HippyTextView *)parentView performTextUpdate];
|
|
}
|
|
}];
|
|
|
|
return parentProperties;
|
|
}
|
|
|
|
- (NSDictionary<NSString *, id> *)processUpdatedProperties:(NSMutableSet<HippyApplierBlock> *)applierBlocks
|
|
virtualApplierBlocks:(__unused NSMutableSet<HippyApplierVirtualBlock> *)virtualApplierBlocks
|
|
parentProperties:(NSDictionary<NSString *, id> *)parentProperties {
|
|
if ([[self hippySuperview] isKindOfClass:[HippyShadowText class]]) {
|
|
return parentProperties;
|
|
}
|
|
|
|
parentProperties = [super processUpdatedProperties:applierBlocks virtualApplierBlocks:virtualApplierBlocks parentProperties:parentProperties];
|
|
|
|
UIEdgeInsets padding = self.paddingAsInsets;
|
|
CGFloat width = self.frame.size.width - (padding.left + padding.right);
|
|
|
|
NSNumber *parentTag = [[self hippySuperview] hippyTag];
|
|
// MTTlayout
|
|
NSTextStorage *textStorage = [self buildTextStorageForWidth:width widthMode:MeasureModeExactly];
|
|
UIColor *color = self.color ?: [UIColor blackColor];
|
|
CGRect textFrame = [self calculateTextFrame:textStorage];
|
|
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
HippyText *view = (HippyText *)viewRegistry[self.hippyTag];
|
|
view.textFrame = textFrame;
|
|
view.textStorage = textStorage;
|
|
view.textColor = color;
|
|
/**
|
|
* 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 HippyShadowText
|
|
* to the HippyText view in time to be used to update the editable text content.
|
|
* TODO: we should establish a delegate relationship betweeen HippyTextView
|
|
* and its contaned HippyText element when they get inserted and get rid of this
|
|
*/
|
|
UIView *parentView = viewRegistry[parentTag];
|
|
if ([parentView respondsToSelector:@selector(performTextUpdate)]) {
|
|
[(HippyTextView *)parentView performTextUpdate];
|
|
}
|
|
}];
|
|
|
|
[virtualApplierBlocks addObject:^(NSDictionary<NSNumber *, HippyVirtualNode *> *virtualRegistry) {
|
|
HippyVirtualTextNode *node = (HippyVirtualTextNode *)virtualRegistry[self.hippyTag];
|
|
node.textFrame = textFrame;
|
|
node.textStorage = textStorage;
|
|
node.textColor = color;
|
|
}];
|
|
|
|
return parentProperties;
|
|
}
|
|
// MTTlayout
|
|
- (void)applyLayoutNode:(MTTNodeRef)node
|
|
viewsWithNewFrame:(NSMutableSet<HippyShadowView *> *)viewsWithNewFrame
|
|
absolutePosition:(CGPoint)absolutePosition {
|
|
[super applyLayoutNode:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition];
|
|
[self dirtyPropagation];
|
|
}
|
|
|
|
// MTTlayout
|
|
- (void)applyLayoutToChildren:(__unused MTTNodeRef)node
|
|
viewsWithNewFrame:(NSMutableSet<HippyShadowView *> *)viewsWithNewFrame
|
|
absolutePosition:(CGPoint)absolutePosition {
|
|
@try {
|
|
// Run layout on subviews.
|
|
UIEdgeInsets padding = self.paddingAsInsets;
|
|
CGFloat width = self.frame.size.width - (padding.left + padding.right);
|
|
NSTextStorage *textStorage = [self buildTextStorageForWidth:width widthMode:MeasureModeExactly];
|
|
CGRect textFrame = [self calculateTextFrame:textStorage];
|
|
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
|
|
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
|
|
[textStorage enumerateAttribute:HippyShadowViewAttributeName inRange:characterRange options:0 usingBlock:^(
|
|
HippyShadowView *child, NSRange range, __unused BOOL *_) {
|
|
if (child) {
|
|
MTTNodeRef childNode = child.nodeRef;
|
|
float width = MTTNodeLayoutGetWidth(childNode);
|
|
float height = MTTNodeLayoutGetHeight(childNode);
|
|
if (isnan(width) || isnan(height)) {
|
|
HippyLogError(@"Views nested within a <Text> must have a width and height");
|
|
}
|
|
// Use line fragment's rect instead of glyph rect for calculation,
|
|
// since we have changed the baselineOffset.
|
|
CGRect lineRect = [layoutManager lineFragmentRectForGlyphAtIndex:range.location
|
|
effectiveRange:nil
|
|
withoutAdditionalLayout:YES];
|
|
CGPoint location = [layoutManager locationForGlyphAtIndex:range.location];
|
|
CGFloat roundedHeight = x5RoundPixelValue(height);
|
|
CGFloat roundedWidth = x5RoundPixelValue(width);
|
|
// take margin into account
|
|
float left = MTTNodeLayoutGetLeft(childNode);
|
|
float top = MTTNodeLayoutGetTop(childNode);
|
|
float marginV = MTTNodeLayoutGetMargin(child.nodeRef, CSSTop) + MTTNodeLayoutGetMargin(child.nodeRef, CSSBottom);
|
|
CGFloat roundedHeightWithMargin = x5RoundPixelValue(height + marginV);
|
|
|
|
CGFloat positionY = .0f;
|
|
NSNumber *verticalAlignType = [textStorage attribute:HippyTextVerticalAlignAttributeName
|
|
atIndex:range.location effectiveRange:nil];
|
|
switch (verticalAlignType.integerValue) {
|
|
case HippyTextVerticalAlignBottom: {
|
|
positionY = CGRectGetMaxY(lineRect) - roundedHeightWithMargin;
|
|
break;
|
|
}
|
|
case HippyTextVerticalAlignUndefined:
|
|
case HippyTextVerticalAlignBaseline: {
|
|
// get baseline-bottom distance from HippyVerticalAlignBaselineOffsetAttributeName
|
|
NSNumber *baselineToBottom = [textStorage attribute:HippyVerticalAlignBaselineOffsetAttributeName
|
|
atIndex:range.location effectiveRange:nullptr];
|
|
positionY = CGRectGetMaxY(lineRect) - roundedHeightWithMargin - baselineToBottom.doubleValue;
|
|
break;
|
|
}
|
|
case HippyTextVerticalAlignTop: {
|
|
positionY = CGRectGetMinY(lineRect);
|
|
break;
|
|
}
|
|
case HippyTextVerticalAlignMiddle: {
|
|
positionY = CGRectGetMinY(lineRect) +
|
|
(CGRectGetHeight(lineRect) - roundedHeightWithMargin) / 2.0f - child.verticalAlignOffset;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
CGRect childFrame = CGRectMake(textFrame.origin.x + location.x + left,
|
|
textFrame.origin.y + positionY + top,
|
|
roundedWidth, roundedHeight);
|
|
NSRange truncatedGlyphRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:range.location];
|
|
BOOL childIsTruncated = NSIntersectionRange(range, truncatedGlyphRange).length != 0;
|
|
|
|
[child collectUpdatedFrames:viewsWithNewFrame withFrame:childFrame hidden:childIsTruncated absolutePosition:absolutePosition];
|
|
}
|
|
}];
|
|
} @catch (NSException *exception) {
|
|
NSLog(@"%@", exception.description);
|
|
}
|
|
}
|
|
|
|
// MTTlayout
|
|
- (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(MeasureMode)widthMode {
|
|
if (isnan(width)) {
|
|
width = 0;
|
|
}
|
|
|
|
if (_cachedTextStorage && width == _cachedTextStorageWidth && widthMode == _cachedTextStorageWidthMode) {
|
|
return _cachedTextStorage;
|
|
}
|
|
|
|
// textContainer
|
|
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 == MeasureModeUndefined ? CGFLOAT_MAX : width, CGFLOAT_MAX };
|
|
|
|
// layoutManager && textStorage
|
|
NSLayoutManager *layoutManager = [NSLayoutManager new];
|
|
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedString];
|
|
[textStorage addLayoutManager:layoutManager];
|
|
|
|
layoutManager.delegate = self;
|
|
[layoutManager addTextContainer:textContainer];
|
|
[layoutManager ensureLayoutForTextContainer:textContainer];
|
|
|
|
// for better perf, only do relayout when MeasureMode is MeasureModeExactly
|
|
if (_needRelayoutText && MeasureModeExactly == widthMode) {
|
|
// relayout text
|
|
[layoutManager invalidateLayoutForCharacterRange:NSMakeRange(0, textStorage.length) actualCharacterRange:nil];
|
|
[layoutManager removeTextContainerAtIndex:0];
|
|
[layoutManager addTextContainer:textContainer];
|
|
[layoutManager ensureLayoutForTextContainer:textContainer];
|
|
_needRelayoutText = NO;
|
|
}
|
|
|
|
if (_autoLetterSpacing) {
|
|
resetFontAttribute(textStorage);
|
|
_cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:textStorage];
|
|
}
|
|
|
|
_cachedTextStorageWidth = width;
|
|
_cachedTextStorageWidthMode = widthMode;
|
|
_cachedTextStorage = textStorage;
|
|
|
|
return textStorage;
|
|
}
|
|
|
|
- (void)dirtyText {
|
|
[super dirtyText];
|
|
_cachedTextStorage = nil;
|
|
}
|
|
|
|
- (void)recomputeText {
|
|
[self attributedString];
|
|
[self setTextComputed];
|
|
[self dirtyPropagation];
|
|
}
|
|
|
|
#pragma mark - AttributeString
|
|
|
|
- (NSAttributedString *)attributedString {
|
|
HippyAttributedStringStyleInfo *info = [HippyAttributedStringStyleInfo new];
|
|
info.foregroundColor = self.color ?: [UIColor blackColor];
|
|
info.backgroundColor = self.backgroundColor;
|
|
info.opacity = self.opacity;
|
|
info.isNestedText = self.hippySubviews.count > 0;
|
|
_isNestedText = info.isNestedText;
|
|
return [self _attributedStringWithStyleInfo:info];
|
|
}
|
|
|
|
- (NSAttributedString *)_attributedStringWithStyleInfo:(HippyAttributedStringStyleInfo *)styleInfo {
|
|
if (![self isTextDirty] && _cachedAttributedString) {
|
|
return _cachedAttributedString;
|
|
}
|
|
|
|
if (_fontSize && !isnan(_fontSize)) {
|
|
styleInfo.fontSize = @(_fontSize);
|
|
}
|
|
else if (nil == (id)styleInfo.fontSize) {
|
|
//default font size is 14
|
|
styleInfo.fontSize = @(gDefaultFontSize);
|
|
}
|
|
if (_fontWeight) {
|
|
styleInfo.fontWeight = _fontWeight;
|
|
}
|
|
if (_fontStyle) {
|
|
styleInfo.fontStyle = _fontStyle;
|
|
}
|
|
if (_fontFamily) {
|
|
styleInfo.fontFamily = _fontFamily;
|
|
}
|
|
if (!isnan(_letterSpacing)) {
|
|
styleInfo.letterSpacing = @(_letterSpacing);
|
|
}
|
|
|
|
_effectiveLetterSpacing = styleInfo.letterSpacing.doubleValue;
|
|
|
|
UIFont *f = nil;
|
|
if (styleInfo.fontFamily) {
|
|
f = [UIFont fontWithName:styleInfo.fontFamily size:[styleInfo.fontSize floatValue]];
|
|
}
|
|
|
|
UIFont *font = [HippyFont updateFont:f withFamily:styleInfo.fontFamily
|
|
size:styleInfo.fontSize
|
|
weight:styleInfo.fontWeight
|
|
style:styleInfo.fontStyle
|
|
variant:_fontVariant
|
|
scaleMultiplier:_allowFontScaling ? _fontSizeMultiplier : 1.0];
|
|
|
|
CGFloat heightOfTallestSubview = 0.0;
|
|
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:self.text ?: @""];
|
|
NSWritingDirection direction = [[HippyI18nUtils sharedInstance] writingDirectionForCurrentAppLanguage];
|
|
for (HippyShadowView *child in [self hippySubviews]) {
|
|
if ([child isKindOfClass:[HippyShadowText class]]) {
|
|
HippyShadowText *shadowText = (HippyShadowText *)child;
|
|
HippyAttributedStringStyleInfo *childInfo = [HippyAttributedStringStyleInfo new];
|
|
childInfo.fontFamily = styleInfo.fontFamily;
|
|
childInfo.fontSize = styleInfo.fontSize;
|
|
childInfo.fontWeight = styleInfo.fontWeight;
|
|
childInfo.fontStyle = styleInfo.fontStyle;
|
|
childInfo.letterSpacing = styleInfo.letterSpacing;
|
|
childInfo.useBackgroundColor = YES;
|
|
childInfo.foregroundColor = [shadowText color] ?: styleInfo.foregroundColor;
|
|
childInfo.backgroundColor = shadowText.backgroundColor ?: styleInfo.backgroundColor;
|
|
childInfo.opacity = styleInfo.opacity * shadowText.opacity;
|
|
childInfo.isNestedText = styleInfo.isNestedText;
|
|
NSAttributedString *subStr = [shadowText _attributedStringWithStyleInfo:childInfo];
|
|
[attributedString appendAttributedString:subStr];
|
|
[child setTextComputed];
|
|
} else {
|
|
// MTTlayout
|
|
MTTDirection nodeDirection = (NSWritingDirectionRightToLeft == direction) ? DirectionRTL : DirectionLTR;
|
|
nodeDirection = self.layoutDirection != DirectionInherit ? self.layoutDirection : nodeDirection;
|
|
MTTNodeDoLayout(child.nodeRef, NAN, NAN, nodeDirection);
|
|
float width = MTTNodeLayoutGetWidth(child.nodeRef);
|
|
float height = MTTNodeLayoutGetHeight(child.nodeRef);
|
|
if (isnan(width) || isnan(height)) {
|
|
HippyLogError(@"Views nested within a <Text> must have a width and height");
|
|
}
|
|
// take margin into account
|
|
float marginH = MTTNodeLayoutGetMargin(child.nodeRef, CSSLeft) + MTTNodeLayoutGetMargin(child.nodeRef, CSSRight);
|
|
float marginV = MTTNodeLayoutGetMargin(child.nodeRef, CSSTop) + MTTNodeLayoutGetMargin(child.nodeRef, CSSBottom);
|
|
width += marginH;
|
|
height += marginV;
|
|
|
|
// create text attachment and append to attachmentString
|
|
static UIImage *placehoderImage = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
placehoderImage = [[UIImage alloc] init];
|
|
});
|
|
NSTextAttachment *attachment = [NSTextAttachment new];
|
|
attachment.bounds = (CGRect) { CGPointZero, { width, height } };
|
|
attachment.image = placehoderImage;
|
|
NSMutableAttributedString *attachmentString = [NSMutableAttributedString new];
|
|
[attachmentString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
|
|
[attachmentString addAttribute:NSFontAttributeName
|
|
value:[UIFont systemFontOfSize:0]
|
|
range:(NSRange) { 0, attachmentString.length }];
|
|
[attachmentString addAttribute:HippyShadowViewAttributeName
|
|
value:child
|
|
range:(NSRange) { 0, attachmentString.length }];
|
|
if (HippyTextVerticalAlignUndefined != child.verticalAlignType) {
|
|
[attachmentString addAttribute:HippyTextVerticalAlignAttributeName
|
|
value:@(child.verticalAlignType)
|
|
range:(NSRange) { 0, attachmentString.length }];
|
|
}
|
|
[attributedString appendAttributedString:attachmentString];
|
|
if (height > heightOfTallestSubview) {
|
|
heightOfTallestSubview = height;
|
|
}
|
|
// Don't call setTextComputed on this child. HippyTextManager takes care of
|
|
// processing inline UIViews.
|
|
}
|
|
}
|
|
|
|
[self _addAttribute:NSForegroundColorAttributeName
|
|
withValue:[styleInfo.foregroundColor
|
|
colorWithAlphaComponent:CGColorGetAlpha(styleInfo.foregroundColor.CGColor) * styleInfo.opacity]
|
|
toAttributedString:attributedString];
|
|
|
|
if (_isHighlighted) {
|
|
[self _addAttribute:HippyIsHighlightedAttributeName withValue:@YES toAttributedString:attributedString];
|
|
}
|
|
if (styleInfo.useBackgroundColor && styleInfo.backgroundColor) {
|
|
[self _addAttribute:NSBackgroundColorAttributeName
|
|
withValue:[styleInfo.backgroundColor
|
|
colorWithAlphaComponent:CGColorGetAlpha(styleInfo.backgroundColor.CGColor) * styleInfo.opacity]
|
|
toAttributedString:attributedString];
|
|
}
|
|
|
|
[self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString];
|
|
[self _addAttribute:NSKernAttributeName withValue:styleInfo.letterSpacing toAttributedString:attributedString];
|
|
[self _addAttribute:HippyHippyTagAttributeName withValue:self.hippyTag toAttributedString:attributedString];
|
|
if (HippyTextVerticalAlignUndefined != self.verticalAlignType) {
|
|
[self _addAttribute:HippyTextVerticalAlignAttributeName
|
|
withValue:@(self.verticalAlignType)
|
|
toAttributedString:attributedString];
|
|
}
|
|
[self _setParagraphStyleOnAttributedString:attributedString
|
|
fontLineHeight:font.lineHeight
|
|
heightOfTallestSubview:heightOfTallestSubview
|
|
isNestedText:styleInfo.isNestedText];
|
|
if (NSWritingDirectionRightToLeft == direction) {
|
|
NSDictionary *dic = @{NSWritingDirectionAttributeName: @[@(NSWritingDirectionRightToLeft | NSWritingDirectionEmbedding)]};
|
|
[attributedString addAttributes:dic range:NSMakeRange(0, [attributedString length])];
|
|
}
|
|
else {
|
|
NSDictionary *dic = @{NSWritingDirectionAttributeName: @[@(NSWritingDirectionLeftToRight | NSWritingDirectionOverride)]};
|
|
[attributedString addAttributes:dic range:NSMakeRange(0, [attributedString length])];
|
|
}
|
|
|
|
// create a non-mutable attributedString for use by the Text system which avoids copies down the line
|
|
_cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:attributedString];
|
|
// MTTlayout
|
|
MTTNodeMarkDirty(self.nodeRef);
|
|
|
|
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, __unused 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
|
|
isNestedText:(BOOL)isNestedText {
|
|
if (fabs(_lineHeight - 0) < DBL_EPSILON) {
|
|
_lineHeight = fontLineHeight;
|
|
}
|
|
|
|
// check if we have lineHeight set on self
|
|
__block BOOL hasParagraphStyle = NO;
|
|
if (_lineHeight || _textAlignSet) {
|
|
hasParagraphStyle = YES;
|
|
}
|
|
|
|
__block float newLineHeight = _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:NSMakeRange(0, attributedString.length)
|
|
options:kNilOptions
|
|
usingBlock:^(id value, __unused NSRange range, __unused BOOL *stop) {
|
|
if (value) {
|
|
NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value;
|
|
CGFloat maximumLineHeight = round(paragraphStyle.maximumLineHeight / fontSizeMultiplier);
|
|
if (maximumLineHeight > newLineHeight) {
|
|
newLineHeight = maximumLineHeight;
|
|
}
|
|
hasParagraphStyle = YES;
|
|
}
|
|
}];
|
|
|
|
if (self.lineHeight != newLineHeight) {
|
|
self.lineHeight = newLineHeight;
|
|
}
|
|
|
|
__block CGFloat maximumFontLineHeight = 0;
|
|
// Note that the font automatically adjusted by text storage needs to be used here,
|
|
// because the lineHeight of some special characters will be higher than normal
|
|
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
|
|
[textStorage enumerateAttribute:NSFontAttributeName
|
|
inRange:NSMakeRange(0, attributedString.length)
|
|
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
|
|
usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
|
|
if (!font) {
|
|
return;
|
|
}
|
|
if (maximumFontLineHeight <= font.lineHeight) {
|
|
maximumFontLineHeight = font.lineHeight;
|
|
}
|
|
}];
|
|
|
|
// if we found anything, set it :D
|
|
if (hasParagraphStyle) {
|
|
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
|
|
paragraphStyle.alignment = _textAlign;
|
|
CGFloat lineHeight = round(_lineHeight * fontSizeMultiplier);
|
|
CGFloat maxHeight = lineHeight;
|
|
if (heightOfTallestSubview > lineHeight) {
|
|
maxHeight = ceilf(heightOfTallestSubview);
|
|
}
|
|
maxHeight = MAX(maxHeight, maximumFontLineHeight);
|
|
paragraphStyle.minimumLineHeight = lineHeight;
|
|
paragraphStyle.maximumLineHeight = maxHeight;
|
|
[attributedString addAttribute:NSParagraphStyleAttributeName
|
|
value:paragraphStyle
|
|
range:NSMakeRange(0, attributedString.length)];
|
|
|
|
/**
|
|
* for keeping text vertical center, we need to set baseline offset
|
|
* Note: baseline offset adjustment of text with attachment or nested Text
|
|
* is in NSLayoutManagerDelegate's imp
|
|
*/
|
|
if (!isNestedText && (lineHeight > fontLineHeight)
|
|
&& HippyTextVerticalAlignUndefined == self.verticalAlignType) {
|
|
CGFloat baselineOffset = (newLineHeight - maximumFontLineHeight) / 2.0f;
|
|
if (baselineOffset > .0f) {
|
|
[attributedString addAttribute:NSBaselineOffsetAttributeName
|
|
value:@(baselineOffset)
|
|
range:NSMakeRange(0, attributedString.length)];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Text decoration
|
|
if (_textDecorationLine == HippyTextDecorationLineTypeUnderline ||
|
|
_textDecorationLine == HippyTextDecorationLineTypeUnderlineStrikethrough) {
|
|
[self _addAttribute:NSUnderlineStyleAttributeName withValue:@(_textDecorationStyle) toAttributedString:attributedString];
|
|
}
|
|
if (_textDecorationLine == HippyTextDecorationLineTypeStrikethrough ||
|
|
_textDecorationLine == HippyTextDecorationLineTypeUnderlineStrikethrough) {
|
|
[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.paddingAsInsets);
|
|
|
|
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.paddingAsInsets.top + HippyRoundPixelValue((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 / HippyTextAutoSizeGranularity)) == round((midScale / HippyTextAutoSizeGranularity))) {
|
|
// Bail because we can't meet error margin.
|
|
return [self calculateSize:textStorage];
|
|
} else {
|
|
HippySizeComparison comparison = [self attemptScale:midScale inStorage:textStorage forFrame:frame];
|
|
if (comparison == HippySizeWithinRange) {
|
|
return [self calculateSize:textStorage];
|
|
} else if (comparison == HippySizeTooLarge) {
|
|
return [self calculateOptimumScaleInFrame:frame forStorage:textStorage minScale:minScale maxScale:midScale - HippyTextAutoSizeGranularity
|
|
prevMid:midScale];
|
|
} else {
|
|
return [self calculateOptimumScaleInFrame:frame forStorage:textStorage minScale:midScale + HippyTextAutoSizeGranularity maxScale:maxScale
|
|
prevMid:midScale];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (HippySizeComparison)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, __unused 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) * HippyTextAutoSizeWidthErrorMargin)) > CGRectGetWidth(frame)
|
|
&& (requiredSize.height + (CGRectGetHeight(frame) * HippyTextAutoSizeHeightErrorMargin)) > CGRectGetHeight(frame)) {
|
|
return HippySizeWithinRange;
|
|
} else {
|
|
return HippySizeTooSmall;
|
|
}
|
|
} else {
|
|
return HippySizeTooLarge;
|
|
}
|
|
}
|
|
|
|
// 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 HIPPY_TEXT_PROPERTY(setProp, ivar, type) \
|
|
-(void)set##setProp : (type)value; \
|
|
{ \
|
|
ivar = value; \
|
|
[self dirtyText]; \
|
|
}
|
|
|
|
HIPPY_TEXT_PROPERTY(AdjustsFontSizeToFit, _adjustsFontSizeToFit, BOOL)
|
|
HIPPY_TEXT_PROPERTY(Color, _color, UIColor *)
|
|
HIPPY_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *)
|
|
HIPPY_TEXT_PROPERTY(FontSize, _fontSize, CGFloat)
|
|
HIPPY_TEXT_PROPERTY(FontWeight, _fontWeight, NSString *)
|
|
HIPPY_TEXT_PROPERTY(FontStyle, _fontStyle, NSString *)
|
|
HIPPY_TEXT_PROPERTY(FontVariant, _fontVariant, NSArray *)
|
|
HIPPY_TEXT_PROPERTY(IsHighlighted, _isHighlighted, BOOL)
|
|
HIPPY_TEXT_PROPERTY(LetterSpacing, _letterSpacing, CGFloat)
|
|
HIPPY_TEXT_PROPERTY(LineHeight, _lineHeight, CGFloat)
|
|
HIPPY_TEXT_PROPERTY(NumberOfLines, _numberOfLines, NSUInteger)
|
|
HIPPY_TEXT_PROPERTY(EllipsizeMode, _ellipsizeMode, NSLineBreakMode)
|
|
HIPPY_TEXT_PROPERTY(TextDecorationColor, _textDecorationColor, UIColor *);
|
|
HIPPY_TEXT_PROPERTY(TextDecorationLine, _textDecorationLine, HippyTextDecorationLineType);
|
|
HIPPY_TEXT_PROPERTY(TextDecorationStyle, _textDecorationStyle, NSUnderlineStyle);
|
|
HIPPY_TEXT_PROPERTY(Opacity, _opacity, CGFloat)
|
|
HIPPY_TEXT_PROPERTY(TextShadowOffset, _textShadowOffset, CGSize);
|
|
HIPPY_TEXT_PROPERTY(TextShadowRadius, _textShadowRadius, CGFloat);
|
|
HIPPY_TEXT_PROPERTY(TextShadowColor, _textShadowColor, UIColor *);
|
|
|
|
- (void)setTextAlign:(NSTextAlignment)textAlign {
|
|
_textAlign = textAlign;
|
|
_textAlignSet = YES;
|
|
[self dirtyText];
|
|
}
|
|
|
|
/*
|
|
* text类型控件会响应用户输入交互,但是并不会更新shadowText中的props属性,
|
|
* 导致前端下发新的props属性与当前属性实际值不一致
|
|
* 因此需要对特定属性进行判断.
|
|
* 这个案例中是text属性
|
|
*/
|
|
- (NSDictionary *)mergeProps:(NSDictionary *)props {
|
|
NSDictionary *newProps = [super mergeProps:props];
|
|
BOOL oldPropsContainsText = [[props allKeys] containsObject:@"text"];
|
|
BOOL newPropsContainsText = [[newProps allKeys] containsObject:@"text"];
|
|
if (!newPropsContainsText && oldPropsContainsText) {
|
|
NSDictionary *textDic = [props dictionaryWithValuesForKeys:@[@"text"]];
|
|
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:newProps];
|
|
[dic addEntriesFromDictionary:textDic];
|
|
newProps = dic;
|
|
}
|
|
return newProps;
|
|
}
|
|
|
|
- (void)setText:(NSString *)text {
|
|
double version = UIDevice.currentDevice.systemVersion.doubleValue;
|
|
if (version >= 10.0 && version < 12.0) {
|
|
text = [text stringByReplacingOccurrencesOfString:@"జ్ఞా" withString:@" "];
|
|
}
|
|
if (_text != text && ![_text isEqualToString:text]) {
|
|
_text = [text copy];
|
|
[self dirtyText];
|
|
}
|
|
}
|
|
|
|
- (void)setAllowFontScaling:(BOOL)allowFontScaling {
|
|
_allowFontScaling = allowFontScaling;
|
|
for (HippyShadowView *child in [self hippySubviews]) {
|
|
if ([child isKindOfClass:[HippyShadowText class]]) {
|
|
((HippyShadowText *)child).allowFontScaling = allowFontScaling;
|
|
}
|
|
}
|
|
[self dirtyText];
|
|
}
|
|
|
|
- (void)setFontSizeMultiplier:(CGFloat)fontSizeMultiplier {
|
|
_fontSizeMultiplier = fontSizeMultiplier;
|
|
if (_fontSizeMultiplier == 0) {
|
|
HippyLogError(@"fontSizeMultiplier value must be > zero.");
|
|
_fontSizeMultiplier = 1.0;
|
|
}
|
|
for (HippyShadowView *child in [self hippySubviews]) {
|
|
if ([child isKindOfClass:[HippyShadowText class]]) {
|
|
((HippyShadowText *)child).fontSizeMultiplier = fontSizeMultiplier;
|
|
}
|
|
}
|
|
[self dirtyText];
|
|
}
|
|
|
|
- (void)setMinimumFontScale:(CGFloat)minimumFontScale {
|
|
if (minimumFontScale >= 0.01) {
|
|
_minimumFontScale = minimumFontScale;
|
|
}
|
|
[self dirtyText];
|
|
}
|
|
|
|
|
|
#pragma mark - NSLayoutManagerDelegate
|
|
|
|
- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect:(inout CGRect *)lineFragmentRect
|
|
lineFragmentUsedRect:(inout CGRect *)lineFragmentUsedRect baselineOffset:(inout CGFloat *)baselineOffset
|
|
inTextContainer:(NSTextContainer *)textContainer forGlyphRange:(NSRange)glyphRange {
|
|
NSTextStorage *textStorage = layoutManager.textStorage;
|
|
if (_isNestedText || HippyTextVerticalAlignUndefined != self.verticalAlignType) {
|
|
__block CGFloat maxAttachmentHeight = .0f;
|
|
__block BOOL hasAttachment = NO;
|
|
NSRange storageRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:nil];
|
|
[textStorage enumerateAttribute:NSAttachmentAttributeName
|
|
inRange:storageRange options:0
|
|
usingBlock:^(NSTextAttachment *attachment, NSRange range, __unused BOOL *_) {
|
|
if (attachment) {
|
|
float height = CGRectGetHeight(attachment.bounds);
|
|
if (height > maxAttachmentHeight) {
|
|
maxAttachmentHeight = height;
|
|
}
|
|
hasAttachment = YES;
|
|
}
|
|
}];
|
|
|
|
__block BOOL hasBaselineAlign = NO;
|
|
[textStorage enumerateAttribute:HippyTextVerticalAlignAttributeName
|
|
inRange:storageRange options:0
|
|
usingBlock:^(NSNumber *type, NSRange range, BOOL * _Nonnull stop) {
|
|
if (HippyTextVerticalAlignBaseline == type.integerValue ||
|
|
HippyTextVerticalAlignUndefined == type.integerValue) {
|
|
hasBaselineAlign = YES;
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
|
|
// find the max font
|
|
CGFloat realBaselineOffset = .0f;
|
|
if (hasBaselineAlign) {
|
|
__block UIFont *maxFont = nil;
|
|
[textStorage enumerateAttribute:NSFontAttributeName
|
|
inRange:storageRange options:0
|
|
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
|
|
UIFont *currentFont = (UIFont *)value;
|
|
if (currentFont) {
|
|
if (!maxFont || currentFont.pointSize > maxFont.pointSize) {
|
|
maxFont = currentFont;
|
|
}
|
|
}
|
|
}];
|
|
// calculate the position of 'baseline' for later layout
|
|
CGFloat textBaselineToBottom = abs(maxFont.descender) + abs(maxFont.leading);
|
|
CGFloat maxTotalHeight = MAX((maxAttachmentHeight + textBaselineToBottom), maxFont.lineHeight);
|
|
realBaselineOffset = (CGRectGetHeight(*lineFragmentUsedRect) - maxTotalHeight) / 2.f;
|
|
if (hasAttachment) {
|
|
[textStorage addAttribute:HippyVerticalAlignBaselineOffsetAttributeName
|
|
value:@(realBaselineOffset + textBaselineToBottom)
|
|
range:storageRange];
|
|
}
|
|
}
|
|
|
|
[textStorage enumerateAttributesInRange:storageRange
|
|
options:kNilOptions
|
|
usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs,
|
|
NSRange range, BOOL * _Nonnull stop) {
|
|
NSNumber *verticalAlignValue = attrs[HippyTextVerticalAlignAttributeName];
|
|
// Calculate position of text
|
|
id offsetValue = [textStorage attribute:NSBaselineOffsetAttributeName
|
|
atIndex:range.location effectiveRange:nil];
|
|
if (!offsetValue) {
|
|
CGFloat offset = .0f;
|
|
CGFloat lineHeight = CGRectGetHeight(*lineFragmentUsedRect);
|
|
CGFloat baselineToBottom = lineHeight - *baselineOffset;
|
|
switch (verticalAlignValue.integerValue) {
|
|
case HippyTextVerticalAlignTop: {
|
|
UIFont *font = attrs[NSFontAttributeName];
|
|
offset = lineHeight - font.ascender - baselineToBottom;
|
|
break;
|
|
}
|
|
case HippyTextVerticalAlignMiddle: {
|
|
UIFont *font = attrs[NSFontAttributeName];
|
|
offset = (lineHeight - font.lineHeight) / 2.f - baselineToBottom
|
|
+ abs(font.descender) + abs(font.leading) + self.verticalAlignOffset;
|
|
break;
|
|
}
|
|
case HippyTextVerticalAlignUndefined:
|
|
case HippyTextVerticalAlignBaseline: {
|
|
offset = realBaselineOffset;
|
|
break;
|
|
}
|
|
case HippyTextVerticalAlignBottom: {
|
|
UIFont *font = attrs[NSFontAttributeName];
|
|
offset = abs(font.descender) + abs(font.leading) - baselineToBottom;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
if (abs(offset) > .0f && !attrs[HippyShadowViewAttributeName]) {
|
|
// only set for Text
|
|
[textStorage addAttribute:NSBaselineOffsetAttributeName value:@(offset) range:range];
|
|
_needRelayoutText = YES;
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
@end
|