1151 lines
37 KiB
Objective-C
Executable File
1151 lines
37 KiB
Objective-C
Executable File
//
|
|
// M80AttributedLabel.m
|
|
// M80AttributedLabel
|
|
//
|
|
// Created by amao on 13-9-1.
|
|
// Copyright (c) 2013年 www.xiangwangfeng.com. All rights reserved.
|
|
//
|
|
|
|
#import "M80AttributedLabel.h"
|
|
#import "M80AttributedLabelAttachment.h"
|
|
#import "M80AttributedLabelURL.h"
|
|
|
|
static NSString* const M80EllipsesCharacter = @"\u2026";
|
|
|
|
static dispatch_queue_t m80_attributed_label_parse_queue;
|
|
static dispatch_queue_t get_m80_attributed_label_parse_queue() \
|
|
{
|
|
if (m80_attributed_label_parse_queue == NULL) {
|
|
m80_attributed_label_parse_queue = dispatch_queue_create("com.m80.parse_queue", 0);
|
|
}
|
|
return m80_attributed_label_parse_queue;
|
|
}
|
|
|
|
@interface M80AttributedLabel ()
|
|
{
|
|
NSMutableArray *_attachments;
|
|
NSMutableArray *_linkLocations;
|
|
CTFrameRef _textFrame;
|
|
CGFloat _fontAscent;
|
|
CGFloat _fontDescent;
|
|
CGFloat _fontHeight;
|
|
}
|
|
@property (nonatomic,strong) NSMutableAttributedString *attributedString;
|
|
@property (nonatomic,strong) M80AttributedLabelURL *touchedLink;
|
|
@property (nonatomic,assign) BOOL linkDetected;
|
|
@property (nonatomic,assign) BOOL ignoreRedraw;
|
|
@end
|
|
|
|
@implementation M80AttributedLabel
|
|
|
|
- (id)initWithFrame:(CGRect)frame
|
|
{
|
|
self = [super initWithFrame:frame];
|
|
if (self)
|
|
{
|
|
[self commonInit];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithCoder:(NSCoder *)aDecoder
|
|
{
|
|
self = [super initWithCoder:aDecoder];
|
|
if (self)
|
|
{
|
|
[self commonInit];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
if (_textFrame)
|
|
{
|
|
CFRelease(_textFrame);
|
|
}
|
|
|
|
}
|
|
|
|
#pragma mark - 初始化
|
|
- (void)commonInit
|
|
{
|
|
_attributedString = [[NSMutableAttributedString alloc]init];
|
|
_attachments = [[NSMutableArray alloc]init];
|
|
_linkLocations = [[NSMutableArray alloc]init];
|
|
_textFrame = nil;
|
|
_linkColor = [UIColor blueColor];
|
|
_font = [UIFont systemFontOfSize:15];
|
|
_textColor = [UIColor blackColor];
|
|
_highlightColor = [UIColor colorWithRed:0xd7/255.0
|
|
green:0xf2/255.0
|
|
blue:0xff/255.0
|
|
alpha:1];
|
|
_lineBreakMode = kCTLineBreakByWordWrapping;
|
|
_underLineForLink = YES;
|
|
_autoDetectLinks = YES;
|
|
_lineSpacing = 0.0;
|
|
_paragraphSpacing = 0.0;
|
|
|
|
if (self.backgroundColor == nil)
|
|
{
|
|
self.backgroundColor = [UIColor whiteColor];
|
|
}
|
|
|
|
self.userInteractionEnabled = YES;
|
|
[self resetFont];
|
|
}
|
|
|
|
- (void)cleanAll
|
|
{
|
|
_ignoreRedraw = NO;
|
|
_linkDetected = NO;
|
|
[_attachments removeAllObjects];
|
|
[_linkLocations removeAllObjects];
|
|
self.touchedLink = nil;
|
|
for (UIView *subView in self.subviews)
|
|
{
|
|
[subView removeFromSuperview];
|
|
}
|
|
[self resetTextFrame];
|
|
}
|
|
|
|
|
|
- (void)resetTextFrame
|
|
{
|
|
if (_textFrame)
|
|
{
|
|
CFRelease(_textFrame);
|
|
_textFrame = nil;
|
|
}
|
|
if ([NSThread isMainThread] && !_ignoreRedraw)
|
|
{
|
|
[self setNeedsDisplay];
|
|
}
|
|
}
|
|
|
|
- (void)resetFont
|
|
{
|
|
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)self.font.fontName, self.font.pointSize, NULL);
|
|
if (fontRef)
|
|
{
|
|
_fontAscent = CTFontGetAscent(fontRef);
|
|
_fontDescent = CTFontGetDescent(fontRef);
|
|
_fontHeight = CTFontGetSize(fontRef);
|
|
CFRelease(fontRef);
|
|
}
|
|
}
|
|
|
|
#pragma mark - 属性设置
|
|
//保证正常绘制,如果传入nil就直接不处理
|
|
- (void)setFont:(UIFont *)font
|
|
{
|
|
if (font && _font != font)
|
|
{
|
|
_font = font;
|
|
|
|
[_attributedString m80_setFont:_font];
|
|
[self resetFont];
|
|
for (M80AttributedLabelAttachment *attachment in _attachments)
|
|
{
|
|
attachment.fontAscent = _fontAscent;
|
|
attachment.fontDescent = _fontDescent;
|
|
}
|
|
[self resetTextFrame];
|
|
}
|
|
}
|
|
|
|
- (void)setTextColor:(UIColor *)textColor
|
|
{
|
|
if (textColor && _textColor != textColor)
|
|
{
|
|
_textColor = textColor;
|
|
[_attributedString m80_setTextColor:textColor];
|
|
[self resetTextFrame];
|
|
}
|
|
}
|
|
|
|
- (void)setHighlightColor:(UIColor *)highlightColor
|
|
{
|
|
if (highlightColor && _highlightColor != highlightColor)
|
|
{
|
|
_highlightColor = highlightColor;
|
|
|
|
[self resetTextFrame];
|
|
}
|
|
}
|
|
|
|
- (void)setLinkColor:(UIColor *)linkColor
|
|
{
|
|
if (_linkColor != linkColor)
|
|
{
|
|
_linkColor = linkColor;
|
|
[self resetTextFrame];
|
|
}
|
|
}
|
|
|
|
- (void)setFrame:(CGRect)frame
|
|
{
|
|
CGRect oldRect = self.bounds;
|
|
[super setFrame:frame];
|
|
|
|
if (!CGRectEqualToRect(self.bounds, oldRect))
|
|
{
|
|
[self resetTextFrame];
|
|
}
|
|
}
|
|
|
|
- (void)setBounds:(CGRect)bounds
|
|
{
|
|
CGRect oldRect = self.bounds;
|
|
[super setBounds:bounds];
|
|
|
|
if (!CGRectEqualToRect(self.bounds, oldRect))
|
|
{
|
|
[self resetTextFrame];
|
|
}
|
|
}
|
|
|
|
- (void)setShadowColor:(UIColor *)shadowColor
|
|
{
|
|
if (_shadowColor != shadowColor)
|
|
{
|
|
_shadowColor = shadowColor;
|
|
[self resetTextFrame];
|
|
}
|
|
}
|
|
|
|
- (void)setShadowOffset:(CGSize)shadowOffset
|
|
{
|
|
if (!CGSizeEqualToSize(_shadowOffset, shadowOffset))
|
|
{
|
|
_shadowOffset = shadowOffset;
|
|
[self resetTextFrame];
|
|
}
|
|
}
|
|
|
|
- (void)setShadowBlur:(CGFloat)shadowBlur
|
|
{
|
|
if (_shadowBlur != shadowBlur)
|
|
{
|
|
_shadowBlur = shadowBlur;
|
|
[self resetTextFrame];
|
|
}
|
|
}
|
|
|
|
#pragma mark - 辅助方法
|
|
- (NSAttributedString *)attributedString:(NSString *)text
|
|
{
|
|
if ([text length])
|
|
{
|
|
NSMutableAttributedString *string = [[NSMutableAttributedString alloc]initWithString:text];
|
|
[string m80_setFont:self.font];
|
|
[string m80_setTextColor:self.textColor];
|
|
return string;
|
|
}
|
|
else
|
|
{
|
|
return [[NSAttributedString alloc] init];
|
|
}
|
|
}
|
|
|
|
- (NSInteger)numberOfDisplayedLines
|
|
{
|
|
CFArrayRef lines = CTFrameGetLines(_textFrame);
|
|
return _numberOfLines > 0 ? MIN(CFArrayGetCount(lines), _numberOfLines) : CFArrayGetCount(lines);
|
|
}
|
|
|
|
- (NSAttributedString *)attributedStringForDraw
|
|
{
|
|
if (_attributedString)
|
|
{
|
|
//添加排版格式
|
|
NSMutableAttributedString *drawString = [_attributedString mutableCopy];
|
|
|
|
//如果LineBreakMode为TranncateTail,那么默认排版模式改成kCTLineBreakByCharWrapping,使得尽可能地显示所有文字
|
|
CTLineBreakMode lineBreakMode = self.lineBreakMode;
|
|
if (self.lineBreakMode == kCTLineBreakByTruncatingTail)
|
|
{
|
|
lineBreakMode = _numberOfLines == 1 ? kCTLineBreakByTruncatingTail : kCTLineBreakByWordWrapping;
|
|
}
|
|
CGFloat fontLineHeight = self.font.lineHeight; //使用全局fontHeight作为最小lineHeight
|
|
|
|
|
|
CTParagraphStyleSetting settings[] =
|
|
{
|
|
{kCTParagraphStyleSpecifierAlignment,sizeof(_textAlignment),&_textAlignment},
|
|
{kCTParagraphStyleSpecifierLineBreakMode,sizeof(lineBreakMode),&lineBreakMode},
|
|
{kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(_lineSpacing),&_lineSpacing},
|
|
{kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(_lineSpacing),&_lineSpacing},
|
|
{kCTParagraphStyleSpecifierParagraphSpacing,sizeof(_paragraphSpacing),&_paragraphSpacing},
|
|
{kCTParagraphStyleSpecifierMinimumLineHeight,sizeof(fontLineHeight),&fontLineHeight},
|
|
};
|
|
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings,sizeof(settings) / sizeof(settings[0]));
|
|
[drawString addAttribute:(id)kCTParagraphStyleAttributeName
|
|
value:(__bridge id)paragraphStyle
|
|
range:NSMakeRange(0, [drawString length])];
|
|
CFRelease(paragraphStyle);
|
|
|
|
|
|
|
|
for (M80AttributedLabelURL *url in _linkLocations)
|
|
{
|
|
if (url.range.location + url.range.length >[_attributedString length])
|
|
{
|
|
continue;
|
|
}
|
|
UIColor *drawLinkColor = url.color ? : self.linkColor;
|
|
[drawString m80_setTextColor:drawLinkColor range:url.range];
|
|
[drawString m80_setUnderlineStyle:_underLineForLink ? kCTUnderlineStyleSingle : kCTUnderlineStyleNone
|
|
modifier:kCTUnderlinePatternSolid
|
|
range:url.range];
|
|
}
|
|
return drawString;
|
|
}
|
|
else
|
|
{
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (M80AttributedLabelURL *)urlForPoint:(CGPoint)point
|
|
{
|
|
static const CGFloat kVMargin = 5;
|
|
if (!CGRectContainsPoint(CGRectInset(self.bounds, 0, -kVMargin), point)
|
|
|| _textFrame == nil)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
CFArrayRef lines = CTFrameGetLines(_textFrame);
|
|
if (!lines)
|
|
return nil;
|
|
CFIndex count = CFArrayGetCount(lines);
|
|
|
|
CGPoint origins[count];
|
|
CTFrameGetLineOrigins(_textFrame, CFRangeMake(0,0), origins);
|
|
|
|
CGAffineTransform transform = [self transformForCoreText];
|
|
CGFloat verticalOffset = 0; //不像Nimbus一样设置文字的对齐方式,都统一是TOP,那么offset就为0
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
CGPoint linePoint = origins[i];
|
|
|
|
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
|
|
CGRect flippedRect = [self getLineBounds:line point:linePoint];
|
|
CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);
|
|
|
|
rect = CGRectInset(rect, 0, -kVMargin);
|
|
rect = CGRectOffset(rect, 0, verticalOffset);
|
|
|
|
if (CGRectContainsPoint(rect, point))
|
|
{
|
|
CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect),
|
|
point.y-CGRectGetMinY(rect));
|
|
CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
|
|
M80AttributedLabelURL *url = [self linkAtIndex:idx];
|
|
if (url)
|
|
{
|
|
return url;
|
|
}
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
|
|
- (id)linkDataForPoint:(CGPoint)point
|
|
{
|
|
M80AttributedLabelURL *url = [self urlForPoint:point];
|
|
return url ? url.linkData : nil;
|
|
}
|
|
|
|
- (CGAffineTransform)transformForCoreText
|
|
{
|
|
return CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1.f, -1.f);
|
|
}
|
|
|
|
- (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint) point
|
|
{
|
|
CGFloat ascent = 0.0f;
|
|
CGFloat descent = 0.0f;
|
|
CGFloat leading = 0.0f;
|
|
CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
|
|
CGFloat height = ascent + descent;
|
|
|
|
return CGRectMake(point.x, point.y - descent, width, height);
|
|
}
|
|
|
|
- (M80AttributedLabelURL *)linkAtIndex:(CFIndex)index
|
|
{
|
|
for (M80AttributedLabelURL *url in _linkLocations)
|
|
{
|
|
if (NSLocationInRange(index, url.range))
|
|
{
|
|
return url;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
|
|
- (CGRect)rectForRange:(NSRange)range
|
|
inLine:(CTLineRef)line
|
|
lineOrigin:(CGPoint)lineOrigin
|
|
{
|
|
CGRect rectForRange = CGRectZero;
|
|
CFArrayRef runs = CTLineGetGlyphRuns(line);
|
|
CFIndex runCount = CFArrayGetCount(runs);
|
|
|
|
// Iterate through each of the "runs" (i.e. a chunk of text) and find the runs that
|
|
// intersect with the range.
|
|
for (CFIndex k = 0; k < runCount; k++)
|
|
{
|
|
CTRunRef run = CFArrayGetValueAtIndex(runs, k);
|
|
|
|
CFRange stringRunRange = CTRunGetStringRange(run);
|
|
NSRange lineRunRange = NSMakeRange(stringRunRange.location, stringRunRange.length);
|
|
NSRange intersectedRunRange = NSIntersectionRange(lineRunRange, range);
|
|
|
|
if (intersectedRunRange.length == 0)
|
|
{
|
|
// This run doesn't intersect the range, so skip it.
|
|
continue;
|
|
}
|
|
|
|
CGFloat ascent = 0.0f;
|
|
CGFloat descent = 0.0f;
|
|
CGFloat leading = 0.0f;
|
|
|
|
// Use of 'leading' doesn't properly highlight Japanese-character link.
|
|
CGFloat width = (CGFloat)CTRunGetTypographicBounds(run,
|
|
CFRangeMake(0, 0),
|
|
&ascent,
|
|
&descent,
|
|
NULL); //&leading);
|
|
CGFloat height = ascent + descent;
|
|
|
|
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil);
|
|
|
|
CGRect linkRect = CGRectMake(lineOrigin.x + xOffset - leading, lineOrigin.y - descent, width + leading, height);
|
|
|
|
linkRect.origin.y = roundf(linkRect.origin.y);
|
|
linkRect.origin.x = roundf(linkRect.origin.x);
|
|
linkRect.size.width = roundf(linkRect.size.width);
|
|
linkRect.size.height = roundf(linkRect.size.height);
|
|
|
|
rectForRange = CGRectIsEmpty(rectForRange) ? linkRect : CGRectUnion(rectForRange, linkRect);
|
|
}
|
|
|
|
return rectForRange;
|
|
}
|
|
|
|
- (void)appendAttachment:(M80AttributedLabelAttachment *)attachment
|
|
{
|
|
attachment.fontAscent = _fontAscent;
|
|
attachment.fontDescent = _fontDescent;
|
|
unichar objectReplacementChar = 0xFFFC;
|
|
NSString *objectReplacementString = [NSString stringWithCharacters:&objectReplacementChar length:1];
|
|
NSMutableAttributedString *attachText = [[NSMutableAttributedString alloc]initWithString:objectReplacementString];
|
|
|
|
CTRunDelegateCallbacks callbacks;
|
|
callbacks.version = kCTRunDelegateVersion1;
|
|
callbacks.getAscent = ascentCallback;
|
|
callbacks.getDescent = descentCallback;
|
|
callbacks.getWidth = widthCallback;
|
|
callbacks.dealloc = deallocCallback;
|
|
|
|
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (void *)attachment);
|
|
NSDictionary *attr = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)delegate,kCTRunDelegateAttributeName, nil];
|
|
[attachText setAttributes:attr range:NSMakeRange(0, 1)];
|
|
CFRelease(delegate);
|
|
|
|
[_attachments addObject:attachment];
|
|
[self appendAttributedText:attachText];
|
|
}
|
|
|
|
|
|
#pragma mark - 设置文本
|
|
- (void)setText:(NSString *)text
|
|
{
|
|
NSAttributedString *attributedText = [self attributedString:text];
|
|
[self setAttributedText:attributedText];
|
|
}
|
|
|
|
- (void)setAttributedText:(NSAttributedString *)attributedText
|
|
{
|
|
_attributedString = [[NSMutableAttributedString alloc]initWithAttributedString:attributedText];
|
|
[self cleanAll];
|
|
}
|
|
|
|
- (NSString *)text
|
|
{
|
|
return [_attributedString string];
|
|
}
|
|
|
|
- (NSAttributedString *)attributedText
|
|
{
|
|
return [_attributedString copy];
|
|
}
|
|
|
|
#pragma mark - 添加文本
|
|
- (void)appendText:(NSString *)text
|
|
{
|
|
NSAttributedString *attributedText = [self attributedString:text];
|
|
[self appendAttributedText:attributedText];
|
|
}
|
|
|
|
- (void)appendAttributedText:(NSAttributedString *)attributedText
|
|
{
|
|
[_attributedString appendAttributedString:attributedText];
|
|
[self resetTextFrame];
|
|
}
|
|
|
|
|
|
#pragma mark - 添加图片
|
|
- (void)appendImage:(UIImage *)image
|
|
{
|
|
[self appendImage:image
|
|
maxSize:image.size];
|
|
}
|
|
|
|
- (void)appendImage:(UIImage *)image
|
|
maxSize:(CGSize)maxSize
|
|
{
|
|
[self appendImage:image
|
|
maxSize:maxSize
|
|
margin:UIEdgeInsetsZero];
|
|
}
|
|
|
|
- (void)appendImage:(UIImage *)image
|
|
maxSize:(CGSize)maxSize
|
|
margin:(UIEdgeInsets)margin
|
|
{
|
|
[self appendImage:image
|
|
maxSize:maxSize
|
|
margin:margin
|
|
alignment:M80ImageAlignmentBottom];
|
|
}
|
|
|
|
- (void)appendImage:(UIImage *)image
|
|
maxSize:(CGSize)maxSize
|
|
margin:(UIEdgeInsets)margin
|
|
alignment:(M80ImageAlignment)alignment
|
|
{
|
|
M80AttributedLabelAttachment *attachment = [M80AttributedLabelAttachment attachmentWith:image
|
|
margin:margin
|
|
alignment:alignment
|
|
maxSize:maxSize];
|
|
[self appendAttachment:attachment];
|
|
}
|
|
|
|
#pragma mark - 添加UI控件
|
|
- (void)appendView:(UIView *)view
|
|
{
|
|
[self appendView:view
|
|
margin:UIEdgeInsetsZero];
|
|
}
|
|
|
|
- (void)appendView:(UIView *)view
|
|
margin:(UIEdgeInsets)margin
|
|
{
|
|
[self appendView:view
|
|
margin:margin
|
|
alignment:M80ImageAlignmentBottom];
|
|
}
|
|
|
|
|
|
- (void)appendView:(UIView *)view
|
|
margin:(UIEdgeInsets)margin
|
|
alignment:(M80ImageAlignment)alignment
|
|
{
|
|
M80AttributedLabelAttachment *attachment = [M80AttributedLabelAttachment attachmentWith:view
|
|
margin:margin
|
|
alignment:alignment
|
|
maxSize:CGSizeZero];
|
|
[self appendAttachment:attachment];
|
|
}
|
|
|
|
#pragma mark - 添加链接
|
|
- (void)addCustomLink:(id)linkData
|
|
forRange:(NSRange)range
|
|
{
|
|
[self addCustomLink:linkData
|
|
forRange:range
|
|
linkColor:self.linkColor];
|
|
|
|
}
|
|
|
|
- (void)addCustomLink:(id)linkData
|
|
forRange:(NSRange)range
|
|
linkColor:(UIColor *)color
|
|
{
|
|
M80AttributedLabelURL *url = [M80AttributedLabelURL urlWithLinkData:linkData
|
|
range:range
|
|
color:color];
|
|
[_linkLocations addObject:url];
|
|
[self resetTextFrame];
|
|
}
|
|
|
|
#pragma mark - 计算大小
|
|
- (CGSize)sizeThatFits:(CGSize)size
|
|
{
|
|
NSAttributedString *drawString = [self attributedStringForDraw];
|
|
if (drawString == nil)
|
|
{
|
|
return CGSizeZero;
|
|
}
|
|
CFAttributedStringRef attributedStringRef = (__bridge CFAttributedStringRef)drawString;
|
|
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedStringRef);
|
|
CFRange range = CFRangeMake(0, 0);
|
|
if (_numberOfLines > 0 && framesetter)
|
|
{
|
|
CGMutablePathRef path = CGPathCreateMutable();
|
|
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
|
|
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
|
|
CFArrayRef lines = CTFrameGetLines(frame);
|
|
|
|
if (nil != lines && CFArrayGetCount(lines) > 0)
|
|
{
|
|
NSInteger lastVisibleLineIndex = MIN(_numberOfLines, CFArrayGetCount(lines)) - 1;
|
|
CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
|
|
|
|
CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
|
|
range = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
|
|
}
|
|
CFRelease(frame);
|
|
CFRelease(path);
|
|
}
|
|
|
|
CFRange fitCFRange = CFRangeMake(0, 0);
|
|
CGSize newSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, range, NULL, size, &fitCFRange);
|
|
if (framesetter)
|
|
{
|
|
CFRelease(framesetter);
|
|
}
|
|
|
|
//hack:
|
|
//1.需要加上额外的一部分size,有些情况下计算出来的像素点并不是那么精准
|
|
//2.iOS7 的 CTFramesetterSuggestFrameSizeWithConstraint s方法比较残,需要多加一部分 height
|
|
//3.iOS7 多行中如果首行带有很多空格,会导致返回的 suggestionWidth 远小于真实 width ,那么多行情况下就是用传入的 width
|
|
if (newSize.height < _fontHeight * 2) //单行
|
|
{
|
|
return CGSizeMake(ceilf(newSize.width) + 2.0, ceilf(newSize.height) + 4.0);
|
|
}
|
|
else
|
|
{
|
|
return CGSizeMake(size.width, ceilf(newSize.height) + 4.0);
|
|
}
|
|
}
|
|
|
|
|
|
- (CGSize)intrinsicContentSize
|
|
{
|
|
return [self sizeThatFits:CGSizeMake(CGRectGetWidth(self.bounds), CGFLOAT_MAX)];
|
|
}
|
|
|
|
|
|
#pragma mark - 绘制方法
|
|
- (void)drawRect:(CGRect)rect
|
|
{
|
|
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
if (ctx == nil)
|
|
{
|
|
return;
|
|
}
|
|
CGContextSaveGState(ctx);
|
|
CGAffineTransform transform = [self transformForCoreText];
|
|
CGContextConcatCTM(ctx, transform);
|
|
|
|
[self recomputeLinksIfNeeded];
|
|
|
|
NSAttributedString *drawString = [self attributedStringForDraw];
|
|
if (drawString)
|
|
{
|
|
[self prepareTextFrame:drawString rect:rect];
|
|
[self drawHighlightWithRect:rect];
|
|
[self drawAttachments];
|
|
[self drawShadow:ctx];
|
|
[self drawText:drawString
|
|
rect:rect
|
|
context:ctx];
|
|
}
|
|
CGContextRestoreGState(ctx);
|
|
}
|
|
|
|
- (void)prepareTextFrame:(NSAttributedString *)string
|
|
rect:(CGRect)rect
|
|
{
|
|
if (_textFrame == nil)
|
|
{
|
|
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)string);
|
|
CGMutablePathRef path = CGPathCreateMutable();
|
|
CGPathAddRect(path, nil,rect);
|
|
_textFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
|
|
CGPathRelease(path);
|
|
CFRelease(framesetter);
|
|
}
|
|
}
|
|
|
|
- (void)drawHighlightWithRect:(CGRect)rect
|
|
{
|
|
if (self.touchedLink && self.highlightColor)
|
|
{
|
|
[self.highlightColor setFill];
|
|
NSRange linkRange = self.touchedLink.range;
|
|
|
|
CFArrayRef lines = CTFrameGetLines(_textFrame);
|
|
CFIndex count = CFArrayGetCount(lines);
|
|
CGPoint lineOrigins[count];
|
|
CTFrameGetLineOrigins(_textFrame, CFRangeMake(0, 0), lineOrigins);
|
|
NSInteger numberOfLines = [self numberOfDisplayedLines];
|
|
|
|
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
|
|
for (CFIndex i = 0; i < numberOfLines; i++)
|
|
{
|
|
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
|
|
|
|
CFRange stringRange = CTLineGetStringRange(line);
|
|
NSRange lineRange = NSMakeRange(stringRange.location, stringRange.length);
|
|
NSRange intersectedRange = NSIntersectionRange(lineRange, linkRange);
|
|
if (intersectedRange.length == 0) {
|
|
continue;
|
|
}
|
|
|
|
CGRect highlightRect = [self rectForRange:linkRange
|
|
inLine:line
|
|
lineOrigin:lineOrigins[i]];
|
|
highlightRect = CGRectOffset(highlightRect, 0, -rect.origin.y);
|
|
if (!CGRectIsEmpty(highlightRect))
|
|
{
|
|
CGFloat pi = (CGFloat)M_PI;
|
|
|
|
CGFloat radius = 1.0f;
|
|
CGContextMoveToPoint(ctx, highlightRect.origin.x, highlightRect.origin.y + radius);
|
|
CGContextAddLineToPoint(ctx, highlightRect.origin.x, highlightRect.origin.y + highlightRect.size.height - radius);
|
|
CGContextAddArc(ctx, highlightRect.origin.x + radius, highlightRect.origin.y + highlightRect.size.height - radius,
|
|
radius, pi, pi / 2.0f, 1.0f);
|
|
CGContextAddLineToPoint(ctx, highlightRect.origin.x + highlightRect.size.width - radius,
|
|
highlightRect.origin.y + highlightRect.size.height);
|
|
CGContextAddArc(ctx, highlightRect.origin.x + highlightRect.size.width - radius,
|
|
highlightRect.origin.y + highlightRect.size.height - radius, radius, pi / 2, 0.0f, 1.0f);
|
|
CGContextAddLineToPoint(ctx, highlightRect.origin.x + highlightRect.size.width, highlightRect.origin.y + radius);
|
|
CGContextAddArc(ctx, highlightRect.origin.x + highlightRect.size.width - radius, highlightRect.origin.y + radius,
|
|
radius, 0.0f, -pi / 2.0f, 1.0f);
|
|
CGContextAddLineToPoint(ctx, highlightRect.origin.x + radius, highlightRect.origin.y);
|
|
CGContextAddArc(ctx, highlightRect.origin.x + radius, highlightRect.origin.y + radius, radius,
|
|
-pi / 2, pi, 1);
|
|
CGContextFillPath(ctx);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
- (void)drawShadow:(CGContextRef)ctx
|
|
{
|
|
if (self.shadowColor)
|
|
{
|
|
CGContextSetShadowWithColor(ctx, self.shadowOffset, self.shadowBlur, self.shadowColor.CGColor);
|
|
}
|
|
}
|
|
|
|
- (void)drawText:(NSAttributedString *)attributedString
|
|
rect:(CGRect)rect
|
|
context:(CGContextRef)context
|
|
{
|
|
if (_textFrame)
|
|
{
|
|
if (_numberOfLines > 0)
|
|
{
|
|
CFArrayRef lines = CTFrameGetLines(_textFrame);
|
|
NSInteger numberOfLines = [self numberOfDisplayedLines];
|
|
|
|
CGPoint lineOrigins[numberOfLines];
|
|
CTFrameGetLineOrigins(_textFrame, CFRangeMake(0, numberOfLines), lineOrigins);
|
|
|
|
for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++)
|
|
{
|
|
CGPoint lineOrigin = lineOrigins[lineIndex];
|
|
CGContextSetTextPosition(context, lineOrigin.x, lineOrigin.y);
|
|
CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
|
|
|
|
BOOL shouldDrawLine = YES;
|
|
if (lineIndex == numberOfLines - 1 &&
|
|
_lineBreakMode == kCTLineBreakByTruncatingTail)
|
|
{
|
|
//找到最后一行并检查是否需要 truncatingTail
|
|
CFRange lastLineRange = CTLineGetStringRange(line);
|
|
if (lastLineRange.location + lastLineRange.length < attributedString.length)
|
|
{
|
|
CTLineTruncationType truncationType = kCTLineTruncationEnd;
|
|
NSUInteger truncationAttributePosition = lastLineRange.location + lastLineRange.length - 1;
|
|
|
|
NSDictionary *tokenAttributes = [attributedString attributesAtIndex:truncationAttributePosition
|
|
effectiveRange:NULL];
|
|
NSAttributedString *tokenString = [[NSAttributedString alloc] initWithString:M80EllipsesCharacter
|
|
attributes:tokenAttributes];
|
|
CTLineRef truncationToken = CTLineCreateWithAttributedString((CFAttributedStringRef)tokenString);
|
|
|
|
NSMutableAttributedString *truncationString = [[attributedString attributedSubstringFromRange:NSMakeRange(lastLineRange.location, lastLineRange.length)] mutableCopy];
|
|
|
|
if (lastLineRange.length > 0)
|
|
{
|
|
//移除掉最后一个对象...(其实这个地方有点问题,也有可能需要移除最后 2 个对象,因为 attachment 宽度的关系)
|
|
[truncationString deleteCharactersInRange:NSMakeRange(lastLineRange.length - 1, 1)];
|
|
}
|
|
[truncationString appendAttributedString:tokenString];
|
|
|
|
|
|
CTLineRef truncationLine = CTLineCreateWithAttributedString((CFAttributedStringRef)truncationString);
|
|
CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, rect.size.width, truncationType, truncationToken);
|
|
if (!truncatedLine)
|
|
{
|
|
truncatedLine = CFRetain(truncationToken);
|
|
}
|
|
CFRelease(truncationLine);
|
|
CFRelease(truncationToken);
|
|
|
|
CTLineDraw(truncatedLine, context);
|
|
CFRelease(truncatedLine);
|
|
|
|
|
|
shouldDrawLine = NO;
|
|
}
|
|
}
|
|
if(shouldDrawLine)
|
|
{
|
|
CTLineDraw(line, context);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CTFrameDraw(_textFrame,context);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void)drawAttachments
|
|
{
|
|
if ([_attachments count] == 0)
|
|
{
|
|
return;
|
|
}
|
|
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
if (ctx == nil)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CFArrayRef lines = CTFrameGetLines(_textFrame);
|
|
CFIndex lineCount = CFArrayGetCount(lines);
|
|
CGPoint lineOrigins[lineCount];
|
|
CTFrameGetLineOrigins(_textFrame, CFRangeMake(0, 0), lineOrigins);
|
|
NSInteger numberOfLines = [self numberOfDisplayedLines];
|
|
for (CFIndex i = 0; i < numberOfLines; i++)
|
|
{
|
|
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
|
|
CFArrayRef runs = CTLineGetGlyphRuns(line);
|
|
CFIndex runCount = CFArrayGetCount(runs);
|
|
CGPoint lineOrigin = lineOrigins[i];
|
|
CGFloat lineAscent;
|
|
CGFloat lineDescent;
|
|
CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, NULL);
|
|
CGFloat lineHeight = lineAscent + lineDescent;
|
|
CGFloat lineBottomY = lineOrigin.y - lineDescent;
|
|
|
|
//遍历以找到对应的 attachment 进行绘制
|
|
for (CFIndex k = 0; k < runCount; k++)
|
|
{
|
|
CTRunRef run = CFArrayGetValueAtIndex(runs, k);
|
|
NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
|
|
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
|
|
if (nil == delegate)
|
|
{
|
|
continue;
|
|
}
|
|
M80AttributedLabelAttachment* attributedImage = (M80AttributedLabelAttachment *)CTRunDelegateGetRefCon(delegate);
|
|
|
|
CGFloat ascent = 0.0f;
|
|
CGFloat descent = 0.0f;
|
|
CGFloat width = (CGFloat)CTRunGetTypographicBounds(run,
|
|
CFRangeMake(0, 0),
|
|
&ascent,
|
|
&descent,
|
|
NULL);
|
|
|
|
CGFloat imageBoxHeight = [attributedImage boxSize].height;
|
|
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil);
|
|
|
|
CGFloat imageBoxOriginY = 0.0f;
|
|
switch (attributedImage.alignment)
|
|
{
|
|
case M80ImageAlignmentTop:
|
|
imageBoxOriginY = lineBottomY + (lineHeight - imageBoxHeight);
|
|
break;
|
|
case M80ImageAlignmentCenter:
|
|
imageBoxOriginY = lineBottomY + (lineHeight - imageBoxHeight) / 2.0;
|
|
break;
|
|
case M80ImageAlignmentBottom:
|
|
imageBoxOriginY = lineBottomY;
|
|
break;
|
|
}
|
|
|
|
CGRect rect = CGRectMake(lineOrigin.x + xOffset, imageBoxOriginY, width, imageBoxHeight);
|
|
UIEdgeInsets flippedMargins = attributedImage.margin;
|
|
CGFloat top = flippedMargins.top;
|
|
flippedMargins.top = flippedMargins.bottom;
|
|
flippedMargins.bottom = top;
|
|
|
|
CGRect attatchmentRect = UIEdgeInsetsInsetRect(rect, flippedMargins);
|
|
|
|
if (i == numberOfLines - 1 &&
|
|
k >= runCount - 2 &&
|
|
_lineBreakMode == kCTLineBreakByTruncatingTail)
|
|
{
|
|
//最后行最后的2个CTRun需要做额外判断
|
|
CGFloat attachmentWidth = CGRectGetWidth(attatchmentRect);
|
|
const CGFloat kMinEllipsesWidth = attachmentWidth;
|
|
if (CGRectGetWidth(self.bounds) - CGRectGetMinX(attatchmentRect) - attachmentWidth < kMinEllipsesWidth)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
id content = attributedImage.content;
|
|
if ([content isKindOfClass:[UIImage class]])
|
|
{
|
|
CGContextDrawImage(ctx, attatchmentRect, ((UIImage *)content).CGImage);
|
|
}
|
|
else if ([content isKindOfClass:[UIView class]])
|
|
{
|
|
UIView *view = (UIView *)content;
|
|
if (view.superview == nil)
|
|
{
|
|
[self addSubview:view];
|
|
}
|
|
CGRect viewFrame = CGRectMake(attatchmentRect.origin.x,
|
|
self.bounds.size.height - attatchmentRect.origin.y - attatchmentRect.size.height,
|
|
attatchmentRect.size.width,
|
|
attatchmentRect.size.height);
|
|
[view setFrame:viewFrame];
|
|
}
|
|
else
|
|
{
|
|
NSLog(@"Attachment Content Not Supported %@",content);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - 点击事件处理
|
|
- (BOOL)onLabelClick:(CGPoint)point
|
|
{
|
|
id linkData = [self linkDataForPoint:point];
|
|
if (linkData)
|
|
{
|
|
if (_delegate && [_delegate respondsToSelector:@selector(m80AttributedLabel:clickedOnLink:)])
|
|
{
|
|
[_delegate m80AttributedLabel:self clickedOnLink:linkData];
|
|
}
|
|
else
|
|
{
|
|
NSURL *url = nil;
|
|
if ([linkData isKindOfClass:[NSString class]])
|
|
{
|
|
url = [NSURL URLWithString:linkData];
|
|
}
|
|
else if([linkData isKindOfClass:[NSURL class]])
|
|
{
|
|
url = linkData;
|
|
}
|
|
if (url)
|
|
{
|
|
[[UIApplication sharedApplication] openURL:url];
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
|
|
#pragma mark - 链接处理
|
|
- (void)recomputeLinksIfNeeded
|
|
{
|
|
const NSInteger kMinHttpLinkLength = 5;
|
|
if (!_autoDetectLinks || _linkDetected)
|
|
{
|
|
return;
|
|
}
|
|
NSString *text = [[_attributedString string] copy];
|
|
NSUInteger length = [text length];
|
|
if (length <= kMinHttpLinkLength)
|
|
{
|
|
return;
|
|
}
|
|
BOOL sync = length <= M80MinAsyncDetectLinkLength;
|
|
[self computeLink:text
|
|
sync:sync];
|
|
}
|
|
|
|
- (void)computeLink:(NSString *)text
|
|
sync:(BOOL)sync
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
typedef void (^LinkBlock) (NSArray *);
|
|
LinkBlock block = ^(NSArray *links)
|
|
{
|
|
weakSelf.linkDetected = YES;
|
|
if ([links count])
|
|
{
|
|
for (M80AttributedLabelURL *link in links)
|
|
{
|
|
[weakSelf addAutoDetectedLink:link];
|
|
}
|
|
[weakSelf resetTextFrame];
|
|
}
|
|
};
|
|
|
|
if (sync)
|
|
{
|
|
_ignoreRedraw = YES;
|
|
NSArray *links = [M80AttributedLabelURL detectLinks:text];
|
|
block(links);
|
|
_ignoreRedraw = NO;
|
|
}
|
|
else
|
|
{
|
|
dispatch_async(get_m80_attributed_label_parse_queue(), ^{
|
|
|
|
NSArray *links = [M80AttributedLabelURL detectLinks:text];
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSString *plainText = [[weakSelf attributedString] string];
|
|
if ([plainText isEqualToString:text])
|
|
{
|
|
block(links);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
- (void)addAutoDetectedLink:(M80AttributedLabelURL *)link
|
|
{
|
|
NSRange range = link.range;
|
|
for (M80AttributedLabelURL *url in _linkLocations)
|
|
{
|
|
if (NSIntersectionRange(range, url.range).length != 0)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
[self addCustomLink:link.linkData
|
|
forRange:link.range];
|
|
}
|
|
|
|
#pragma mark - 点击事件相应
|
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
|
|
{
|
|
if (self.touchedLink == nil)
|
|
{
|
|
UITouch *touch = [touches anyObject];
|
|
CGPoint point = [touch locationInView:self];
|
|
self.touchedLink = [self urlForPoint:point];
|
|
}
|
|
|
|
|
|
if (self.touchedLink)
|
|
{
|
|
[self setNeedsDisplay];
|
|
}
|
|
else
|
|
{
|
|
[super touchesBegan:touches withEvent:event];
|
|
}
|
|
}
|
|
|
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
|
|
{
|
|
[super touchesMoved:touches withEvent:event];
|
|
UITouch *touch = [touches anyObject];
|
|
CGPoint point = [touch locationInView:self];
|
|
|
|
M80AttributedLabelURL *touchedLink = [self urlForPoint:point];
|
|
if (self.touchedLink != touchedLink)
|
|
{
|
|
self.touchedLink = touchedLink;
|
|
[self setNeedsDisplay];
|
|
}
|
|
}
|
|
|
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
|
|
{
|
|
[super touchesCancelled:touches withEvent:event];
|
|
if (self.touchedLink)
|
|
{
|
|
self.touchedLink = nil;
|
|
[self setNeedsDisplay];
|
|
}
|
|
}
|
|
|
|
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
|
|
{
|
|
UITouch *touch = [touches anyObject];
|
|
CGPoint point = [touch locationInView:self];
|
|
if(![self onLabelClick:point])
|
|
{
|
|
[super touchesEnded:touches withEvent:event];
|
|
}
|
|
if (self.touchedLink)
|
|
{
|
|
self.touchedLink = nil;
|
|
[self setNeedsDisplay];
|
|
}
|
|
}
|
|
|
|
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
|
|
{
|
|
M80AttributedLabelURL *touchedLink = [self urlForPoint:point];
|
|
if (touchedLink == nil)
|
|
{
|
|
NSArray *subViews = [self subviews];
|
|
for (UIView *view in subViews)
|
|
{
|
|
CGPoint hitPoint = [view convertPoint:point
|
|
fromView:self];
|
|
|
|
UIView *hitTestView = [view hitTest:hitPoint
|
|
withEvent:event];
|
|
if (hitTestView)
|
|
{
|
|
return hitTestView;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
else
|
|
{
|
|
return self;
|
|
}
|
|
}
|
|
|
|
#pragma mark - 设置自定义的连接检测block
|
|
+ (void)setCustomDetectMethod:(M80CustomDetectLinkBlock)block
|
|
{
|
|
[M80AttributedLabelURL setCustomDetectMethod:block];
|
|
}
|
|
|
|
|
|
@end
|