New 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 |