Files
2025-08-08 11:05:33 +08:00

1307 lines
47 KiB
Objective-C
Executable File

//
// YYLabel.m
// YYText <https://github.com/ibireme/YYText>
//
// Created by ibireme on 15/2/25.
// Copyright (c) 2015 ibireme.
//
// This source code is licensed under the MIT-style license found in the
// LICENSE file in the root directory of this source tree.
//
#import "YYLabel.h"
#import "YYTextAsyncLayer.h"
#import "YYTextWeakProxy.h"
#import "YYTextUtilities.h"
#import "NSAttributedString+YYText.h"
#import <libkern/OSAtomic.h>
static dispatch_queue_t YYLabelGetReleaseQueue() {
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
}
#define kLongPressMinimumDuration 0.5 // Time in seconds the fingers must be held down for long press gesture.
#define kLongPressAllowableMovement 9.0 // Maximum movement in points allowed before the long press fails.
#define kHighlightFadeDuration 0.15 // Time in seconds for highlight fadeout animation.
#define kAsyncFadeDuration 0.08 // Time in seconds for async display fadeout animation.
@interface YYLabel() <YYTextDebugTarget, YYTextAsyncLayerDelegate> {
NSMutableAttributedString *_innerText; ///< nonnull
YYTextLayout *_innerLayout;
YYTextContainer *_innerContainer; ///< nonnull
NSMutableArray *_attachmentViews;
NSMutableArray *_attachmentLayers;
NSRange _highlightRange; ///< current highlight range
YYTextHighlight *_highlight; ///< highlight attribute in `_highlightRange`
YYTextLayout *_highlightLayout; ///< when _state.showingHighlight=YES, this layout should be displayed
YYTextLayout *_shrinkInnerLayout;
YYTextLayout *_shrinkHighlightLayout;
NSTimer *_longPressTimer;
CGPoint _touchBeganPoint;
struct {
unsigned int layoutNeedUpdate : 1;
unsigned int showingHighlight : 1;
unsigned int trackingTouch : 1;
unsigned int swallowTouch : 1;
unsigned int touchMoved : 1;
unsigned int hasTapAction : 1;
unsigned int hasLongPressAction : 1;
unsigned int contentsNeedFade : 1;
} _state;
}
@end
@implementation YYLabel
#pragma mark - Private
- (void)_updateIfNeeded {
if (_state.layoutNeedUpdate) {
_state.layoutNeedUpdate = NO;
[self _updateLayout];
[self.layer setNeedsDisplay];
}
}
- (void)_updateLayout {
_innerLayout = [YYTextLayout layoutWithContainer:_innerContainer text:_innerText];
_shrinkInnerLayout = [YYLabel _shrinkLayoutWithLayout:_innerLayout];
}
- (void)_setLayoutNeedUpdate {
_state.layoutNeedUpdate = YES;
[self _clearInnerLayout];
[self _setLayoutNeedRedraw];
}
- (void)_setLayoutNeedRedraw {
[self.layer setNeedsDisplay];
}
- (void)_clearInnerLayout {
if (!_innerLayout) return;
YYTextLayout *layout = _innerLayout;
_innerLayout = nil;
_shrinkInnerLayout = nil;
dispatch_async(YYLabelGetReleaseQueue(), ^{
NSAttributedString *text = [layout text]; // capture to block and release in background
if (layout.attachments.count) {
dispatch_async(dispatch_get_main_queue(), ^{
[text length]; // capture to block and release in main thread (maybe there's UIView/CALayer attachments).
});
}
});
}
- (YYTextLayout *)_innerLayout {
return _shrinkInnerLayout ? _shrinkInnerLayout : _innerLayout;
}
- (YYTextLayout *)_highlightLayout {
return _shrinkHighlightLayout ? _shrinkHighlightLayout : _highlightLayout;
}
+ (YYTextLayout *)_shrinkLayoutWithLayout:(YYTextLayout *)layout {
if (layout.text.length && layout.lines.count == 0) {
YYTextContainer *container = layout.container.copy;
container.maximumNumberOfRows = 1;
CGSize containerSize = container.size;
if (!container.verticalForm) {
containerSize.height = YYTextContainerMaxSize.height;
} else {
containerSize.width = YYTextContainerMaxSize.width;
}
container.size = containerSize;
return [YYTextLayout layoutWithContainer:container text:layout.text];
} else {
return nil;
}
}
- (void)_startLongPressTimer {
[_longPressTimer invalidate];
_longPressTimer = [NSTimer timerWithTimeInterval:kLongPressMinimumDuration
target:[YYTextWeakProxy proxyWithTarget:self]
selector:@selector(_trackDidLongPress)
userInfo:nil
repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:_longPressTimer forMode:NSRunLoopCommonModes];
}
- (void)_endLongPressTimer {
[_longPressTimer invalidate];
_longPressTimer = nil;
}
- (void)_trackDidLongPress {
[self _endLongPressTimer];
if (_state.hasLongPressAction && _textLongPressAction) {
NSRange range = NSMakeRange(NSNotFound, 0);
CGRect rect = CGRectNull;
CGPoint point = [self _convertPointToLayout:_touchBeganPoint];
YYTextRange *textRange = [self._innerLayout textRangeAtPoint:point];
CGRect textRect = [self._innerLayout rectForRange:textRange];
textRect = [self _convertRectFromLayout:textRect];
if (textRange) {
range = textRange.asRange;
rect = textRect;
}
_textLongPressAction(self, _innerText, range, rect);
}
if (_highlight) {
YYTextAction longPressAction = _highlight.longPressAction ? _highlight.longPressAction : _highlightLongPressAction;
if (longPressAction) {
YYTextPosition *start = [YYTextPosition positionWithOffset:_highlightRange.location];
YYTextPosition *end = [YYTextPosition positionWithOffset:_highlightRange.location + _highlightRange.length affinity:YYTextAffinityBackward];
YYTextRange *range = [YYTextRange rangeWithStart:start end:end];
CGRect rect = [self._innerLayout rectForRange:range];
rect = [self _convertRectFromLayout:rect];
longPressAction(self, _innerText, _highlightRange, rect);
[self _removeHighlightAnimated:YES];
_state.trackingTouch = NO;
}
}
}
- (YYTextHighlight *)_getHighlightAtPoint:(CGPoint)point range:(NSRangePointer)range {
if (!self._innerLayout.containsHighlight) return nil;
point = [self _convertPointToLayout:point];
YYTextRange *textRange = [self._innerLayout textRangeAtPoint:point];
if (!textRange) return nil;
NSUInteger startIndex = textRange.start.offset;
if (startIndex == _innerText.length) {
if (startIndex > 0) {
startIndex--;
}
}
NSRange highlightRange = {0};
YYTextHighlight *highlight = [_innerText attribute:YYTextHighlightAttributeName
atIndex:startIndex
longestEffectiveRange:&highlightRange
inRange:NSMakeRange(0, _innerText.length)];
if (!highlight) return nil;
if (range) *range = highlightRange;
return highlight;
}
- (void)_showHighlightAnimated:(BOOL)animated {
if (!_highlight) return;
if (!_highlightLayout) {
NSMutableAttributedString *hiText = _innerText.mutableCopy;
NSDictionary *newAttrs = _highlight.attributes;
[newAttrs enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
[hiText yy_setAttribute:key value:value range:_highlightRange];
}];
_highlightLayout = [YYTextLayout layoutWithContainer:_innerContainer text:hiText];
_shrinkHighlightLayout = [YYLabel _shrinkLayoutWithLayout:_highlightLayout];
if (!_highlightLayout) _highlight = nil;
}
if (_highlightLayout && !_state.showingHighlight) {
_state.showingHighlight = YES;
_state.contentsNeedFade = animated;
[self _setLayoutNeedRedraw];
}
}
- (void)_hideHighlightAnimated:(BOOL)animated {
if (_state.showingHighlight) {
_state.showingHighlight = NO;
_state.contentsNeedFade = animated;
[self _setLayoutNeedRedraw];
}
}
- (void)_removeHighlightAnimated:(BOOL)animated {
[self _hideHighlightAnimated:animated];
_highlight = nil;
_highlightLayout = nil;
_shrinkHighlightLayout = nil;
}
- (void)_endTouch {
[self _endLongPressTimer];
[self _removeHighlightAnimated:YES];
_state.trackingTouch = NO;
}
- (CGPoint)_convertPointToLayout:(CGPoint)point {
CGSize boundingSize = self._innerLayout.textBoundingSize;
if (self._innerLayout.container.isVerticalForm) {
CGFloat w = self._innerLayout.textBoundingSize.width;
if (w < self.bounds.size.width) w = self.bounds.size.width;
point.x += self._innerLayout.container.size.width - w;
if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
point.x += (self.bounds.size.width - boundingSize.width) * 0.5;
} else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
point.x += (self.bounds.size.width - boundingSize.width);
}
return point;
} else {
if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
point.y -= (self.bounds.size.height - boundingSize.height) * 0.5;
} else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
point.y -= (self.bounds.size.height - boundingSize.height);
}
return point;
}
}
- (CGPoint)_convertPointFromLayout:(CGPoint)point {
CGSize boundingSize = self._innerLayout.textBoundingSize;
if (self._innerLayout.container.isVerticalForm) {
CGFloat w = self._innerLayout.textBoundingSize.width;
if (w < self.bounds.size.width) w = self.bounds.size.width;
point.x -= self._innerLayout.container.size.width - w;
if (boundingSize.width < self.bounds.size.width) {
if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
point.x -= (self.bounds.size.width - boundingSize.width) * 0.5;
} else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
point.x -= (self.bounds.size.width - boundingSize.width);
}
}
return point;
} else {
if (boundingSize.height < self.bounds.size.height) {
if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
point.y += (self.bounds.size.height - boundingSize.height) * 0.5;
} else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
point.y += (self.bounds.size.height - boundingSize.height);
}
}
return point;
}
}
- (CGRect)_convertRectToLayout:(CGRect)rect {
rect.origin = [self _convertPointToLayout:rect.origin];
return rect;
}
- (CGRect)_convertRectFromLayout:(CGRect)rect {
rect.origin = [self _convertPointFromLayout:rect.origin];
return rect;
}
- (UIFont *)_defaultFont {
return [UIFont systemFontOfSize:17];
}
- (NSShadow *)_shadowFromProperties {
if (!_shadowColor || _shadowBlurRadius < 0) return nil;
NSShadow *shadow = [NSShadow new];
shadow.shadowColor = _shadowColor;
#if !TARGET_INTERFACE_BUILDER
shadow.shadowOffset = _shadowOffset;
#else
shadow.shadowOffset = CGSizeMake(_shadowOffset.x, _shadowOffset.y);
#endif
shadow.shadowBlurRadius = _shadowBlurRadius;
return shadow;
}
- (void)_updateOuterLineBreakMode {
if (_innerContainer.truncationType) {
switch (_innerContainer.truncationType) {
case YYTextTruncationTypeStart: {
_lineBreakMode = NSLineBreakByTruncatingHead;
} break;
case YYTextTruncationTypeEnd: {
_lineBreakMode = NSLineBreakByTruncatingTail;
} break;
case YYTextTruncationTypeMiddle: {
_lineBreakMode = NSLineBreakByTruncatingMiddle;
} break;
default:break;
}
} else {
_lineBreakMode = _innerText.yy_lineBreakMode;
}
}
- (void)_updateOuterTextProperties {
_text = [_innerText yy_plainTextForRange:NSMakeRange(0, _innerText.length)];
_font = _innerText.yy_font;
if (!_font) _font = [self _defaultFont];
_textColor = _innerText.yy_color;
if (!_textColor) _textColor = [UIColor blackColor];
_textAlignment = _innerText.yy_alignment;
_lineBreakMode = _innerText.yy_lineBreakMode;
NSShadow *shadow = _innerText.yy_shadow;
_shadowColor = shadow.shadowColor;
#if !TARGET_INTERFACE_BUILDER
_shadowOffset = shadow.shadowOffset;
#else
_shadowOffset = CGPointMake(shadow.shadowOffset.width, shadow.shadowOffset.height);
#endif
_shadowBlurRadius = shadow.shadowBlurRadius;
_attributedText = _innerText;
[self _updateOuterLineBreakMode];
}
- (void)_updateOuterContainerProperties {
_truncationToken = _innerContainer.truncationToken;
_numberOfLines = _innerContainer.maximumNumberOfRows;
_textContainerPath = _innerContainer.path;
_exclusionPaths = _innerContainer.exclusionPaths;
_textContainerInset = _innerContainer.insets;
_verticalForm = _innerContainer.verticalForm;
_linePositionModifier = _innerContainer.linePositionModifier;
[self _updateOuterLineBreakMode];
}
- (void)_clearContents {
CGImageRef image = (__bridge_retained CGImageRef)(self.layer.contents);
self.layer.contents = nil;
if (image) {
dispatch_async(YYLabelGetReleaseQueue(), ^{
CFRelease(image);
});
}
}
- (void)_initLabel {
((YYTextAsyncLayer *)self.layer).displaysAsynchronously = NO;
self.layer.contentsScale = [UIScreen mainScreen].scale;
self.contentMode = UIViewContentModeRedraw;
_attachmentViews = [NSMutableArray new];
_attachmentLayers = [NSMutableArray new];
_debugOption = [YYTextDebugOption sharedDebugOption];
[YYTextDebugOption addDebugTarget:self];
_font = [self _defaultFont];
_textColor = [UIColor blackColor];
_textVerticalAlignment = YYTextVerticalAlignmentCenter;
_numberOfLines = 1;
_textAlignment = NSTextAlignmentNatural;
_lineBreakMode = NSLineBreakByTruncatingTail;
_innerText = [NSMutableAttributedString new];
_innerContainer = [YYTextContainer new];
_innerContainer.truncationType = YYTextTruncationTypeEnd;
_innerContainer.maximumNumberOfRows = _numberOfLines;
_clearContentsBeforeAsynchronouslyDisplay = YES;
_fadeOnAsynchronouslyDisplay = YES;
_fadeOnHighlight = YES;
self.isAccessibilityElement = YES;
}
#pragma mark - Override
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:CGRectZero];
if (!self) return nil;
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
[self _initLabel];
self.frame = frame;
return self;
}
- (void)dealloc {
[YYTextDebugOption removeDebugTarget:self];
[_longPressTimer invalidate];
}
+ (Class)layerClass {
return [YYTextAsyncLayer class];
}
- (void)setFrame:(CGRect)frame {
CGSize oldSize = self.bounds.size;
[super setFrame:frame];
CGSize newSize = self.bounds.size;
if (!CGSizeEqualToSize(oldSize, newSize)) {
_innerContainer.size = self.bounds.size;
if (!_ignoreCommonProperties) {
_state.layoutNeedUpdate = YES;
}
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedRedraw];
}
}
- (void)setBounds:(CGRect)bounds {
CGSize oldSize = self.bounds.size;
[super setBounds:bounds];
CGSize newSize = self.bounds.size;
if (!CGSizeEqualToSize(oldSize, newSize)) {
_innerContainer.size = self.bounds.size;
if (!_ignoreCommonProperties) {
_state.layoutNeedUpdate = YES;
}
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedRedraw];
}
}
- (CGSize)sizeThatFits:(CGSize)size {
if (_ignoreCommonProperties) {
return _innerLayout.textBoundingSize;
}
if (!_verticalForm && size.width <= 0) size.width = YYTextContainerMaxSize.width;
if (_verticalForm && size.height <= 0) size.height = YYTextContainerMaxSize.height;
if ((!_verticalForm && size.width == self.bounds.size.width) ||
(_verticalForm && size.height == self.bounds.size.height)) {
[self _updateIfNeeded];
YYTextLayout *layout = self._innerLayout;
BOOL contains = NO;
if (layout.container.maximumNumberOfRows == 0) {
if (layout.truncatedLine == nil) {
contains = YES;
}
} else {
if (layout.rowCount <= layout.container.maximumNumberOfRows) {
contains = YES;
}
}
if (contains) {
return layout.textBoundingSize;
}
}
if (!_verticalForm) {
size.height = YYTextContainerMaxSize.height;
} else {
size.width = YYTextContainerMaxSize.width;
}
YYTextContainer *container = [_innerContainer copy];
container.size = size;
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
return layout.textBoundingSize;
}
- (NSString *)accessibilityLabel {
return [_innerLayout.text yy_plainTextForRange:_innerLayout.text.yy_rangeOfAll];
}
#pragma mark - NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
[aCoder encodeObject:_attributedText forKey:@"attributedText"];
[aCoder encodeObject:_innerContainer forKey:@"innerContainer"];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
[self _initLabel];
YYTextContainer *innerContainer = [aDecoder decodeObjectForKey:@"innerContainer"];
if (innerContainer) {
_innerContainer = innerContainer;
} else {
_innerContainer.size = self.bounds.size;
}
[self _updateOuterContainerProperties];
self.attributedText = [aDecoder decodeObjectForKey:@"attributedText"];
[self _setLayoutNeedUpdate];
return self;
}
#pragma mark - Touches
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self _updateIfNeeded];
UITouch *touch = touches.anyObject;
CGPoint point = [touch locationInView:self];
_highlight = [self _getHighlightAtPoint:point range:&_highlightRange];
_highlightLayout = nil;
_shrinkHighlightLayout = nil;
_state.hasTapAction = _textTapAction != nil;
_state.hasLongPressAction = _textLongPressAction != nil;
if (_highlight || _textTapAction || _textLongPressAction) {
_touchBeganPoint = point;
_state.trackingTouch = YES;
_state.swallowTouch = YES;
_state.touchMoved = NO;
[self _startLongPressTimer];
if (_highlight) [self _showHighlightAnimated:NO];
} else {
_state.trackingTouch = NO;
_state.swallowTouch = NO;
_state.touchMoved = NO;
}
if (!_state.swallowTouch) {
[super touchesBegan:touches withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self _updateIfNeeded];
UITouch *touch = touches.anyObject;
CGPoint point = [touch locationInView:self];
if (_state.trackingTouch) {
if (!_state.touchMoved) {
CGFloat moveH = point.x - _touchBeganPoint.x;
CGFloat moveV = point.y - _touchBeganPoint.y;
if (fabs(moveH) > fabs(moveV)) {
if (fabs(moveH) > kLongPressAllowableMovement) _state.touchMoved = YES;
} else {
if (fabs(moveV) > kLongPressAllowableMovement) _state.touchMoved = YES;
}
if (_state.touchMoved) {
[self _endLongPressTimer];
}
}
if (_state.touchMoved && _highlight) {
YYTextHighlight *highlight = [self _getHighlightAtPoint:point range:NULL];
if (highlight == _highlight) {
[self _showHighlightAnimated:_fadeOnHighlight];
} else {
[self _hideHighlightAnimated:_fadeOnHighlight];
}
}
}
if (!_state.swallowTouch) {
[super touchesMoved:touches withEvent:event];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = touches.anyObject;
CGPoint point = [touch locationInView:self];
if (_state.trackingTouch) {
[self _endLongPressTimer];
if (!_state.touchMoved && _textTapAction) {
NSRange range = NSMakeRange(NSNotFound, 0);
CGRect rect = CGRectNull;
CGPoint point = [self _convertPointToLayout:_touchBeganPoint];
YYTextRange *textRange = [self._innerLayout textRangeAtPoint:point];
CGRect textRect = [self._innerLayout rectForRange:textRange];
textRect = [self _convertRectFromLayout:textRect];
if (textRange) {
range = textRange.asRange;
rect = textRect;
}
_textTapAction(self, _innerText, range, rect);
}
if (_highlight) {
if (!_state.touchMoved || [self _getHighlightAtPoint:point range:NULL] == _highlight) {
YYTextAction tapAction = _highlight.tapAction ? _highlight.tapAction : _highlightTapAction;
if (tapAction) {
YYTextPosition *start = [YYTextPosition positionWithOffset:_highlightRange.location];
YYTextPosition *end = [YYTextPosition positionWithOffset:_highlightRange.location + _highlightRange.length affinity:YYTextAffinityBackward];
YYTextRange *range = [YYTextRange rangeWithStart:start end:end];
CGRect rect = [self._innerLayout rectForRange:range];
rect = [self _convertRectFromLayout:rect];
tapAction(self, _innerText, _highlightRange, rect);
}
}
[self _removeHighlightAnimated:_fadeOnHighlight];
}
}
if (!_state.swallowTouch) {
[super touchesEnded:touches withEvent:event];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[self _endTouch];
if (!_state.swallowTouch) [super touchesCancelled:touches withEvent:event];
}
#pragma mark - Properties
- (void)setText:(NSString *)text {
if (_text == text || [_text isEqualToString:text]) return;
_text = text.copy;
BOOL needAddAttributes = _innerText.length == 0 && text.length > 0;
[_innerText replaceCharactersInRange:NSMakeRange(0, _innerText.length) withString:text ? text : @""];
[_innerText yy_removeDiscontinuousAttributesInRange:NSMakeRange(0, _innerText.length)];
if (needAddAttributes) {
_innerText.yy_font = _font;
_innerText.yy_color = _textColor;
_innerText.yy_shadow = [self _shadowFromProperties];
_innerText.yy_alignment = _textAlignment;
switch (_lineBreakMode) {
case NSLineBreakByWordWrapping:
case NSLineBreakByCharWrapping:
case NSLineBreakByClipping: {
_innerText.yy_lineBreakMode = _lineBreakMode;
} break;
case NSLineBreakByTruncatingHead:
case NSLineBreakByTruncatingTail:
case NSLineBreakByTruncatingMiddle: {
_innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
} break;
default: break;
}
}
if ([_textParser parseText:_innerText selectedRange:NULL]) {
[self _updateOuterTextProperties];
}
if (!_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setFont:(UIFont *)font {
if (!font) {
font = [self _defaultFont];
}
if (_font == font || [_font isEqual:font]) return;
_font = font;
_innerText.yy_font = _font;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setTextColor:(UIColor *)textColor {
if (!textColor) {
textColor = [UIColor blackColor];
}
if (_textColor == textColor || [_textColor isEqual:textColor]) return;
_textColor = textColor;
_innerText.yy_color = textColor;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
}
}
- (void)setShadowColor:(UIColor *)shadowColor {
if (_shadowColor == shadowColor || [_shadowColor isEqual:shadowColor]) return;
_shadowColor = shadowColor;
_innerText.yy_shadow = [self _shadowFromProperties];
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
}
}
#if !TARGET_INTERFACE_BUILDER
- (void)setShadowOffset:(CGSize)shadowOffset {
if (CGSizeEqualToSize(_shadowOffset, shadowOffset)) return;
_shadowOffset = shadowOffset;
_innerText.yy_shadow = [self _shadowFromProperties];
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
}
}
#else
- (void)setShadowOffset:(CGPoint)shadowOffset {
if (CGPointEqualToPoint(_shadowOffset, shadowOffset)) return;
_shadowOffset = shadowOffset;
_innerText.yy_shadow = [self _shadowFromProperties];
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
}
}
#endif
- (void)setShadowBlurRadius:(CGFloat)shadowBlurRadius {
if (_shadowBlurRadius == shadowBlurRadius) return;
_shadowBlurRadius = shadowBlurRadius;
_innerText.yy_shadow = [self _shadowFromProperties];
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
}
}
- (void)setTextAlignment:(NSTextAlignment)textAlignment {
if (_textAlignment == textAlignment) return;
_textAlignment = textAlignment;
_innerText.yy_alignment = textAlignment;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode {
if (_lineBreakMode == lineBreakMode) return;
_lineBreakMode = lineBreakMode;
_innerText.yy_lineBreakMode = lineBreakMode;
// allow multi-line break
switch (lineBreakMode) {
case NSLineBreakByWordWrapping:
case NSLineBreakByCharWrapping:
case NSLineBreakByClipping: {
_innerContainer.truncationType = YYTextTruncationTypeNone;
_innerText.yy_lineBreakMode = lineBreakMode;
} break;
case NSLineBreakByTruncatingHead:{
_innerContainer.truncationType = YYTextTruncationTypeStart;
_innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
} break;
case NSLineBreakByTruncatingTail:{
_innerContainer.truncationType = YYTextTruncationTypeEnd;
_innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
} break;
case NSLineBreakByTruncatingMiddle: {
_innerContainer.truncationType = YYTextTruncationTypeMiddle;
_innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
} break;
default: break;
}
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setTextVerticalAlignment:(YYTextVerticalAlignment)textVerticalAlignment {
if (_textVerticalAlignment == textVerticalAlignment) return;
_textVerticalAlignment = textVerticalAlignment;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setTruncationToken:(NSAttributedString *)truncationToken {
if (_truncationToken == truncationToken || [_truncationToken isEqual:truncationToken]) return;
_truncationToken = truncationToken.copy;
_innerContainer.truncationToken = truncationToken;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setNumberOfLines:(NSUInteger)numberOfLines {
if (_numberOfLines == numberOfLines) return;
_numberOfLines = numberOfLines;
_innerContainer.maximumNumberOfRows = numberOfLines;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
if (attributedText.length > 0) {
_innerText = attributedText.mutableCopy;
switch (_lineBreakMode) {
case NSLineBreakByWordWrapping:
case NSLineBreakByCharWrapping:
case NSLineBreakByClipping: {
_innerText.yy_lineBreakMode = _lineBreakMode;
} break;
case NSLineBreakByTruncatingHead:
case NSLineBreakByTruncatingTail:
case NSLineBreakByTruncatingMiddle: {
_innerText.yy_lineBreakMode = NSLineBreakByWordWrapping;
} break;
default: break;
}
} else {
_innerText = [NSMutableAttributedString new];
}
[_textParser parseText:_innerText selectedRange:NULL];
if (!_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _updateOuterTextProperties];
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setTextContainerPath:(UIBezierPath *)textContainerPath {
if (_textContainerPath == textContainerPath || [_textContainerPath isEqual:textContainerPath]) return;
_textContainerPath = textContainerPath.copy;
_innerContainer.path = textContainerPath;
if (!_textContainerPath) {
_innerContainer.size = self.bounds.size;
_innerContainer.insets = _textContainerInset;
}
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setExclusionPaths:(NSArray *)exclusionPaths {
if (_exclusionPaths == exclusionPaths || [_exclusionPaths isEqual:exclusionPaths]) return;
_exclusionPaths = exclusionPaths.copy;
_innerContainer.exclusionPaths = exclusionPaths;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset {
if (UIEdgeInsetsEqualToEdgeInsets(_textContainerInset, textContainerInset)) return;
_textContainerInset = textContainerInset;
_innerContainer.insets = textContainerInset;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setVerticalForm:(BOOL)verticalForm {
if (_verticalForm == verticalForm) return;
_verticalForm = verticalForm;
_innerContainer.verticalForm = verticalForm;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier {
if (_linePositionModifier == linePositionModifier) return;
_linePositionModifier = linePositionModifier;
_innerContainer.linePositionModifier = linePositionModifier;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
- (void)setTextParser:(id<YYTextParser>)textParser {
if (_textParser == textParser || [_textParser isEqual:textParser]) return;
_textParser = textParser;
if ([_textParser parseText:_innerText selectedRange:NULL]) {
[self _updateOuterTextProperties];
if (!_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
[self _setLayoutNeedUpdate];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
}
}
- (void)setTextLayout:(YYTextLayout *)textLayout {
_innerLayout = textLayout;
_shrinkInnerLayout = nil;
if (_ignoreCommonProperties) {
_innerText = (NSMutableAttributedString *)textLayout.text;
_innerContainer = textLayout.container.copy;
} else {
_innerText = textLayout.text.mutableCopy;
if (!_innerText) {
_innerText = [NSMutableAttributedString new];
}
[self _updateOuterTextProperties];
_innerContainer = textLayout.container.copy;
if (!_innerContainer) {
_innerContainer = [YYTextContainer new];
_innerContainer.size = self.bounds.size;
_innerContainer.insets = self.textContainerInset;
}
[self _updateOuterContainerProperties];
}
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents];
}
_state.layoutNeedUpdate = NO;
[self _setLayoutNeedRedraw];
[self _endTouch];
[self invalidateIntrinsicContentSize];
}
- (YYTextLayout *)textLayout {
[self _updateIfNeeded];
return _innerLayout;
}
- (void)setDisplaysAsynchronously:(BOOL)displaysAsynchronously {
_displaysAsynchronously = displaysAsynchronously;
((YYTextAsyncLayer *)self.layer).displaysAsynchronously = displaysAsynchronously;
}
#pragma mark - AutoLayout
- (void)setPreferredMaxLayoutWidth:(CGFloat)preferredMaxLayoutWidth {
if (_preferredMaxLayoutWidth == preferredMaxLayoutWidth) return;
_preferredMaxLayoutWidth = preferredMaxLayoutWidth;
[self invalidateIntrinsicContentSize];
}
- (CGSize)intrinsicContentSize {
if (_preferredMaxLayoutWidth == 0) {
YYTextContainer *container = [_innerContainer copy];
container.size = YYTextContainerMaxSize;
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
return layout.textBoundingSize;
}
CGSize containerSize = _innerContainer.size;
if (!_verticalForm) {
containerSize.height = YYTextContainerMaxSize.height;
containerSize.width = _preferredMaxLayoutWidth;
if (containerSize.width == 0) containerSize.width = self.bounds.size.width;
} else {
containerSize.width = YYTextContainerMaxSize.width;
containerSize.height = _preferredMaxLayoutWidth;
if (containerSize.height == 0) containerSize.height = self.bounds.size.height;
}
YYTextContainer *container = [_innerContainer copy];
container.size = containerSize;
YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
return layout.textBoundingSize;
}
#pragma mark - YYTextDebugTarget
- (void)setDebugOption:(YYTextDebugOption *)debugOption {
BOOL needDraw = _debugOption.needDrawDebug;
_debugOption = debugOption.copy;
if (_debugOption.needDrawDebug != needDraw) {
[self _setLayoutNeedRedraw];
}
}
#pragma mark - YYTextAsyncLayerDelegate
- (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask {
// capture current context
BOOL contentsNeedFade = _state.contentsNeedFade;
NSAttributedString *text = _innerText;
YYTextContainer *container = _innerContainer;
YYTextVerticalAlignment verticalAlignment = _textVerticalAlignment;
YYTextDebugOption *debug = _debugOption;
NSMutableArray *attachmentViews = _attachmentViews;
NSMutableArray *attachmentLayers = _attachmentLayers;
BOOL layoutNeedUpdate = _state.layoutNeedUpdate;
BOOL fadeForAsync = _displaysAsynchronously && _fadeOnAsynchronouslyDisplay;
__block YYTextLayout *layout = (_state.showingHighlight && _highlightLayout) ? self._highlightLayout : self._innerLayout;
__block YYTextLayout *shrinkLayout = nil;
__block BOOL layoutUpdated = NO;
if (layoutNeedUpdate) {
text = text.copy;
container = container.copy;
}
// create display task
YYTextAsyncLayerDisplayTask *task = [YYTextAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer *layer) {
[layer removeAnimationForKey:@"contents"];
// If the attachment is not in new layout, or we don't know the new layout currently,
// the attachment should be removed.
for (UIView *view in attachmentViews) {
if (layoutNeedUpdate || ![layout.attachmentContentsSet containsObject:view]) {
if (view.superview == self) {
[view removeFromSuperview];
}
}
}
for (CALayer *layer in attachmentLayers) {
if (layoutNeedUpdate || ![layout.attachmentContentsSet containsObject:layer]) {
if (layer.superlayer == self.layer) {
[layer removeFromSuperlayer];
}
}
}
[attachmentViews removeAllObjects];
[attachmentLayers removeAllObjects];
};
task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
if (isCancelled()) return;
if (text.length == 0) return;
YYTextLayout *drawLayout = layout;
if (layoutNeedUpdate) {
layout = [YYTextLayout layoutWithContainer:container text:text];
shrinkLayout = [YYLabel _shrinkLayoutWithLayout:layout];
if (isCancelled()) return;
layoutUpdated = YES;
drawLayout = shrinkLayout ? shrinkLayout : layout;
}
CGSize boundingSize = drawLayout.textBoundingSize;
CGPoint point = CGPointZero;
if (verticalAlignment == YYTextVerticalAlignmentCenter) {
if (drawLayout.container.isVerticalForm) {
point.x = -(size.width - boundingSize.width) * 0.5;
} else {
point.y = (size.height - boundingSize.height) * 0.5;
}
} else if (verticalAlignment == YYTextVerticalAlignmentBottom) {
if (drawLayout.container.isVerticalForm) {
point.x = -(size.width - boundingSize.width);
} else {
point.y = (size.height - boundingSize.height);
}
}
point = YYTextCGPointPixelRound(point);
[drawLayout drawInContext:context size:size point:point view:nil layer:nil debug:debug cancel:isCancelled];
};
task.didDisplay = ^(CALayer *layer, BOOL finished) {
YYTextLayout *drawLayout = layout;
if (layoutUpdated && shrinkLayout) {
drawLayout = shrinkLayout;
}
if (!finished) {
// If the display task is cancelled, we should clear the attachments.
for (YYTextAttachment *a in drawLayout.attachments) {
if ([a.content isKindOfClass:[UIView class]]) {
if (((UIView *)a.content).superview == layer.delegate) {
[((UIView *)a.content) removeFromSuperview];
}
} else if ([a.content isKindOfClass:[CALayer class]]) {
if (((CALayer *)a.content).superlayer == layer) {
[((CALayer *)a.content) removeFromSuperlayer];
}
}
}
return;
}
[layer removeAnimationForKey:@"contents"];
__strong YYLabel *view = (YYLabel *)layer.delegate;
if (!view) return;
if (view->_state.layoutNeedUpdate && layoutUpdated) {
view->_innerLayout = layout;
view->_shrinkInnerLayout = shrinkLayout;
view->_state.layoutNeedUpdate = NO;
}
CGSize size = layer.bounds.size;
CGSize boundingSize = drawLayout.textBoundingSize;
CGPoint point = CGPointZero;
if (verticalAlignment == YYTextVerticalAlignmentCenter) {
if (drawLayout.container.isVerticalForm) {
point.x = -(size.width - boundingSize.width) * 0.5;
} else {
point.y = (size.height - boundingSize.height) * 0.5;
}
} else if (verticalAlignment == YYTextVerticalAlignmentBottom) {
if (drawLayout.container.isVerticalForm) {
point.x = -(size.width - boundingSize.width);
} else {
point.y = (size.height - boundingSize.height);
}
}
point = YYTextCGPointPixelRound(point);
[drawLayout drawInContext:nil size:size point:point view:view layer:layer debug:nil cancel:NULL];
for (YYTextAttachment *a in drawLayout.attachments) {
if ([a.content isKindOfClass:[UIView class]]) [attachmentViews addObject:a.content];
else if ([a.content isKindOfClass:[CALayer class]]) [attachmentLayers addObject:a.content];
}
if (contentsNeedFade) {
CATransition *transition = [CATransition animation];
transition.duration = kHighlightFadeDuration;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
transition.type = kCATransitionFade;
[layer addAnimation:transition forKey:@"contents"];
} else if (fadeForAsync) {
CATransition *transition = [CATransition animation];
transition.duration = kAsyncFadeDuration;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
transition.type = kCATransitionFade;
[layer addAnimation:transition forKey:@"contents"];
}
};
return task;
}
@end
@interface YYLabel(IBInspectableProperties)
@end
@implementation YYLabel (IBInspectableProperties)
- (BOOL)fontIsBold_:(UIFont *)font {
if (![font respondsToSelector:@selector(fontDescriptor)]) return NO;
return (font.fontDescriptor.symbolicTraits & UIFontDescriptorTraitBold) > 0;
}
- (UIFont *)boldFont_:(UIFont *)font {
if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize];
}
- (UIFont *)normalFont_:(UIFont *)font {
if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:0] size:font.pointSize];
}
- (void)setFontName_:(NSString *)fontName {
if (!fontName) return;
UIFont *font = self.font;
if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
font = [UIFont systemFontOfSize:font.pointSize];
} else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
font = [UIFont boldSystemFontOfSize:font.pointSize];
} else {
if ([self fontIsBold_:font] && ([fontName.lowercaseString rangeOfString:@"bold"].location == NSNotFound)) {
font = [UIFont fontWithName:fontName size:font.pointSize];
font = [self boldFont_:font];
} else {
font = [UIFont fontWithName:fontName size:font.pointSize];
}
}
if (font) self.font = font;
}
- (void)setFontSize_:(CGFloat)fontSize {
if (fontSize <= 0) return;
UIFont *font = self.font;
font = [font fontWithSize:fontSize];
if (font) self.font = font;
}
- (void)setFontIsBold_:(BOOL)fontBold {
UIFont *font = self.font;
if ([self fontIsBold_:font] == fontBold) return;
if (fontBold) {
font = [self boldFont_:font];
} else {
font = [self normalFont_:font];
}
if (font) self.font = font;
}
- (void)setInsetTop_:(CGFloat)textInsetTop {
UIEdgeInsets insets = self.textContainerInset;
insets.top = textInsetTop;
self.textContainerInset = insets;
}
- (void)setInsetBottom_:(CGFloat)textInsetBottom {
UIEdgeInsets insets = self.textContainerInset;
insets.bottom = textInsetBottom;
self.textContainerInset = insets;
}
- (void)setInsetLeft_:(CGFloat)textInsetLeft {
UIEdgeInsets insets = self.textContainerInset;
insets.left = textInsetLeft;
self.textContainerInset = insets;
}
- (void)setInsetRight_:(CGFloat)textInsetRight {
UIEdgeInsets insets = self.textContainerInset;
insets.right = textInsetRight;
self.textContainerInset = insets;
}
- (void)setDebugEnabled_:(BOOL)enabled {
if (!enabled) {
self.debugOption = nil;
} else {
YYTextDebugOption *debugOption = [YYTextDebugOption new];
debugOption.baselineColor = [UIColor redColor];
debugOption.CTFrameBorderColor = [UIColor redColor];
debugOption.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180];
debugOption.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200];
self.debugOption = debugOption;
}
}
@end