M80AttributedLabel.m 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150
  1. //
  2. // M80AttributedLabel.m
  3. // M80AttributedLabel
  4. //
  5. // Created by amao on 13-9-1.
  6. // Copyright (c) 2013年 www.xiangwangfeng.com. All rights reserved.
  7. //
  8. #import "M80AttributedLabel.h"
  9. #import "M80AttributedLabelAttachment.h"
  10. #import "M80AttributedLabelURL.h"
  11. static NSString* const M80EllipsesCharacter = @"\u2026";
  12. static dispatch_queue_t m80_attributed_label_parse_queue;
  13. static dispatch_queue_t get_m80_attributed_label_parse_queue() \
  14. {
  15. if (m80_attributed_label_parse_queue == NULL) {
  16. m80_attributed_label_parse_queue = dispatch_queue_create("com.m80.parse_queue", 0);
  17. }
  18. return m80_attributed_label_parse_queue;
  19. }
  20. @interface M80AttributedLabel ()
  21. {
  22. NSMutableArray *_attachments;
  23. NSMutableArray *_linkLocations;
  24. CTFrameRef _textFrame;
  25. CGFloat _fontAscent;
  26. CGFloat _fontDescent;
  27. CGFloat _fontHeight;
  28. }
  29. @property (nonatomic,strong) NSMutableAttributedString *attributedString;
  30. @property (nonatomic,strong) M80AttributedLabelURL *touchedLink;
  31. @property (nonatomic,assign) BOOL linkDetected;
  32. @property (nonatomic,assign) BOOL ignoreRedraw;
  33. @end
  34. @implementation M80AttributedLabel
  35. - (id)initWithFrame:(CGRect)frame
  36. {
  37. self = [super initWithFrame:frame];
  38. if (self)
  39. {
  40. [self commonInit];
  41. }
  42. return self;
  43. }
  44. - (id)initWithCoder:(NSCoder *)aDecoder
  45. {
  46. self = [super initWithCoder:aDecoder];
  47. if (self)
  48. {
  49. [self commonInit];
  50. }
  51. return self;
  52. }
  53. - (void)dealloc
  54. {
  55. if (_textFrame)
  56. {
  57. CFRelease(_textFrame);
  58. }
  59. }
  60. #pragma mark - 初始化
  61. - (void)commonInit
  62. {
  63. _attributedString = [[NSMutableAttributedString alloc]init];
  64. _attachments = [[NSMutableArray alloc]init];
  65. _linkLocations = [[NSMutableArray alloc]init];
  66. _textFrame = nil;
  67. _linkColor = [UIColor blueColor];
  68. _font = [UIFont systemFontOfSize:15];
  69. _textColor = [UIColor blackColor];
  70. _highlightColor = [UIColor colorWithRed:0xd7/255.0
  71. green:0xf2/255.0
  72. blue:0xff/255.0
  73. alpha:1];
  74. _lineBreakMode = kCTLineBreakByWordWrapping;
  75. _underLineForLink = YES;
  76. _autoDetectLinks = YES;
  77. _lineSpacing = 0.0;
  78. _paragraphSpacing = 0.0;
  79. if (self.backgroundColor == nil)
  80. {
  81. self.backgroundColor = [UIColor whiteColor];
  82. }
  83. self.userInteractionEnabled = YES;
  84. [self resetFont];
  85. }
  86. - (void)cleanAll
  87. {
  88. _ignoreRedraw = NO;
  89. _linkDetected = NO;
  90. [_attachments removeAllObjects];
  91. [_linkLocations removeAllObjects];
  92. self.touchedLink = nil;
  93. for (UIView *subView in self.subviews)
  94. {
  95. [subView removeFromSuperview];
  96. }
  97. [self resetTextFrame];
  98. }
  99. - (void)resetTextFrame
  100. {
  101. if (_textFrame)
  102. {
  103. CFRelease(_textFrame);
  104. _textFrame = nil;
  105. }
  106. if ([NSThread isMainThread] && !_ignoreRedraw)
  107. {
  108. [self setNeedsDisplay];
  109. }
  110. }
  111. - (void)resetFont
  112. {
  113. CTFontRef fontRef = CTFontCreateWithName((CFStringRef)self.font.fontName, self.font.pointSize, NULL);
  114. if (fontRef)
  115. {
  116. _fontAscent = CTFontGetAscent(fontRef);
  117. _fontDescent = CTFontGetDescent(fontRef);
  118. _fontHeight = CTFontGetSize(fontRef);
  119. CFRelease(fontRef);
  120. }
  121. }
  122. #pragma mark - 属性设置
  123. //保证正常绘制,如果传入nil就直接不处理
  124. - (void)setFont:(UIFont *)font
  125. {
  126. if (font && _font != font)
  127. {
  128. _font = font;
  129. [_attributedString m80_setFont:_font];
  130. [self resetFont];
  131. for (M80AttributedLabelAttachment *attachment in _attachments)
  132. {
  133. attachment.fontAscent = _fontAscent;
  134. attachment.fontDescent = _fontDescent;
  135. }
  136. [self resetTextFrame];
  137. }
  138. }
  139. - (void)setTextColor:(UIColor *)textColor
  140. {
  141. if (textColor && _textColor != textColor)
  142. {
  143. _textColor = textColor;
  144. [_attributedString m80_setTextColor:textColor];
  145. [self resetTextFrame];
  146. }
  147. }
  148. - (void)setHighlightColor:(UIColor *)highlightColor
  149. {
  150. if (highlightColor && _highlightColor != highlightColor)
  151. {
  152. _highlightColor = highlightColor;
  153. [self resetTextFrame];
  154. }
  155. }
  156. - (void)setLinkColor:(UIColor *)linkColor
  157. {
  158. if (_linkColor != linkColor)
  159. {
  160. _linkColor = linkColor;
  161. [self resetTextFrame];
  162. }
  163. }
  164. - (void)setFrame:(CGRect)frame
  165. {
  166. CGRect oldRect = self.bounds;
  167. [super setFrame:frame];
  168. if (!CGRectEqualToRect(self.bounds, oldRect))
  169. {
  170. [self resetTextFrame];
  171. }
  172. }
  173. - (void)setBounds:(CGRect)bounds
  174. {
  175. CGRect oldRect = self.bounds;
  176. [super setBounds:bounds];
  177. if (!CGRectEqualToRect(self.bounds, oldRect))
  178. {
  179. [self resetTextFrame];
  180. }
  181. }
  182. - (void)setShadowColor:(UIColor *)shadowColor
  183. {
  184. if (_shadowColor != shadowColor)
  185. {
  186. _shadowColor = shadowColor;
  187. [self resetTextFrame];
  188. }
  189. }
  190. - (void)setShadowOffset:(CGSize)shadowOffset
  191. {
  192. if (!CGSizeEqualToSize(_shadowOffset, shadowOffset))
  193. {
  194. _shadowOffset = shadowOffset;
  195. [self resetTextFrame];
  196. }
  197. }
  198. - (void)setShadowBlur:(CGFloat)shadowBlur
  199. {
  200. if (_shadowBlur != shadowBlur)
  201. {
  202. _shadowBlur = shadowBlur;
  203. [self resetTextFrame];
  204. }
  205. }
  206. #pragma mark - 辅助方法
  207. - (NSAttributedString *)attributedString:(NSString *)text
  208. {
  209. if ([text length])
  210. {
  211. NSMutableAttributedString *string = [[NSMutableAttributedString alloc]initWithString:text];
  212. [string m80_setFont:self.font];
  213. [string m80_setTextColor:self.textColor];
  214. return string;
  215. }
  216. else
  217. {
  218. return [[NSAttributedString alloc] init];
  219. }
  220. }
  221. - (NSInteger)numberOfDisplayedLines
  222. {
  223. CFArrayRef lines = CTFrameGetLines(_textFrame);
  224. return _numberOfLines > 0 ? MIN(CFArrayGetCount(lines), _numberOfLines) : CFArrayGetCount(lines);
  225. }
  226. - (NSAttributedString *)attributedStringForDraw
  227. {
  228. if (_attributedString)
  229. {
  230. //添加排版格式
  231. NSMutableAttributedString *drawString = [_attributedString mutableCopy];
  232. //如果LineBreakMode为TranncateTail,那么默认排版模式改成kCTLineBreakByCharWrapping,使得尽可能地显示所有文字
  233. CTLineBreakMode lineBreakMode = self.lineBreakMode;
  234. if (self.lineBreakMode == kCTLineBreakByTruncatingTail)
  235. {
  236. lineBreakMode = _numberOfLines == 1 ? kCTLineBreakByTruncatingTail : kCTLineBreakByWordWrapping;
  237. }
  238. CGFloat fontLineHeight = self.font.lineHeight; //使用全局fontHeight作为最小lineHeight
  239. CTParagraphStyleSetting settings[] =
  240. {
  241. {kCTParagraphStyleSpecifierAlignment,sizeof(_textAlignment),&_textAlignment},
  242. {kCTParagraphStyleSpecifierLineBreakMode,sizeof(lineBreakMode),&lineBreakMode},
  243. {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(_lineSpacing),&_lineSpacing},
  244. {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(_lineSpacing),&_lineSpacing},
  245. {kCTParagraphStyleSpecifierParagraphSpacing,sizeof(_paragraphSpacing),&_paragraphSpacing},
  246. {kCTParagraphStyleSpecifierMinimumLineHeight,sizeof(fontLineHeight),&fontLineHeight},
  247. };
  248. CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings,sizeof(settings) / sizeof(settings[0]));
  249. [drawString addAttribute:(id)kCTParagraphStyleAttributeName
  250. value:(__bridge id)paragraphStyle
  251. range:NSMakeRange(0, [drawString length])];
  252. CFRelease(paragraphStyle);
  253. for (M80AttributedLabelURL *url in _linkLocations)
  254. {
  255. if (url.range.location + url.range.length >[_attributedString length])
  256. {
  257. continue;
  258. }
  259. UIColor *drawLinkColor = url.color ? : self.linkColor;
  260. [drawString m80_setTextColor:drawLinkColor range:url.range];
  261. [drawString m80_setUnderlineStyle:_underLineForLink ? kCTUnderlineStyleSingle : kCTUnderlineStyleNone
  262. modifier:kCTUnderlinePatternSolid
  263. range:url.range];
  264. }
  265. return drawString;
  266. }
  267. else
  268. {
  269. return nil;
  270. }
  271. }
  272. - (M80AttributedLabelURL *)urlForPoint:(CGPoint)point
  273. {
  274. static const CGFloat kVMargin = 5;
  275. if (!CGRectContainsPoint(CGRectInset(self.bounds, 0, -kVMargin), point)
  276. || _textFrame == nil)
  277. {
  278. return nil;
  279. }
  280. CFArrayRef lines = CTFrameGetLines(_textFrame);
  281. if (!lines)
  282. return nil;
  283. CFIndex count = CFArrayGetCount(lines);
  284. CGPoint origins[count];
  285. CTFrameGetLineOrigins(_textFrame, CFRangeMake(0,0), origins);
  286. CGAffineTransform transform = [self transformForCoreText];
  287. CGFloat verticalOffset = 0; //不像Nimbus一样设置文字的对齐方式,都统一是TOP,那么offset就为0
  288. for (int i = 0; i < count; i++)
  289. {
  290. CGPoint linePoint = origins[i];
  291. CTLineRef line = CFArrayGetValueAtIndex(lines, i);
  292. CGRect flippedRect = [self getLineBounds:line point:linePoint];
  293. CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);
  294. rect = CGRectInset(rect, 0, -kVMargin);
  295. rect = CGRectOffset(rect, 0, verticalOffset);
  296. if (CGRectContainsPoint(rect, point))
  297. {
  298. CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect),
  299. point.y-CGRectGetMinY(rect));
  300. CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
  301. M80AttributedLabelURL *url = [self linkAtIndex:idx];
  302. if (url)
  303. {
  304. return url;
  305. }
  306. }
  307. }
  308. return nil;
  309. }
  310. - (id)linkDataForPoint:(CGPoint)point
  311. {
  312. M80AttributedLabelURL *url = [self urlForPoint:point];
  313. return url ? url.linkData : nil;
  314. }
  315. - (CGAffineTransform)transformForCoreText
  316. {
  317. return CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1.f, -1.f);
  318. }
  319. - (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint) point
  320. {
  321. CGFloat ascent = 0.0f;
  322. CGFloat descent = 0.0f;
  323. CGFloat leading = 0.0f;
  324. CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
  325. CGFloat height = ascent + descent;
  326. return CGRectMake(point.x, point.y - descent, width, height);
  327. }
  328. - (M80AttributedLabelURL *)linkAtIndex:(CFIndex)index
  329. {
  330. for (M80AttributedLabelURL *url in _linkLocations)
  331. {
  332. if (NSLocationInRange(index, url.range))
  333. {
  334. return url;
  335. }
  336. }
  337. return nil;
  338. }
  339. - (CGRect)rectForRange:(NSRange)range
  340. inLine:(CTLineRef)line
  341. lineOrigin:(CGPoint)lineOrigin
  342. {
  343. CGRect rectForRange = CGRectZero;
  344. CFArrayRef runs = CTLineGetGlyphRuns(line);
  345. CFIndex runCount = CFArrayGetCount(runs);
  346. // Iterate through each of the "runs" (i.e. a chunk of text) and find the runs that
  347. // intersect with the range.
  348. for (CFIndex k = 0; k < runCount; k++)
  349. {
  350. CTRunRef run = CFArrayGetValueAtIndex(runs, k);
  351. CFRange stringRunRange = CTRunGetStringRange(run);
  352. NSRange lineRunRange = NSMakeRange(stringRunRange.location, stringRunRange.length);
  353. NSRange intersectedRunRange = NSIntersectionRange(lineRunRange, range);
  354. if (intersectedRunRange.length == 0)
  355. {
  356. // This run doesn't intersect the range, so skip it.
  357. continue;
  358. }
  359. CGFloat ascent = 0.0f;
  360. CGFloat descent = 0.0f;
  361. CGFloat leading = 0.0f;
  362. // Use of 'leading' doesn't properly highlight Japanese-character link.
  363. CGFloat width = (CGFloat)CTRunGetTypographicBounds(run,
  364. CFRangeMake(0, 0),
  365. &ascent,
  366. &descent,
  367. NULL); //&leading);
  368. CGFloat height = ascent + descent;
  369. CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil);
  370. CGRect linkRect = CGRectMake(lineOrigin.x + xOffset - leading, lineOrigin.y - descent, width + leading, height);
  371. linkRect.origin.y = roundf(linkRect.origin.y);
  372. linkRect.origin.x = roundf(linkRect.origin.x);
  373. linkRect.size.width = roundf(linkRect.size.width);
  374. linkRect.size.height = roundf(linkRect.size.height);
  375. rectForRange = CGRectIsEmpty(rectForRange) ? linkRect : CGRectUnion(rectForRange, linkRect);
  376. }
  377. return rectForRange;
  378. }
  379. - (void)appendAttachment:(M80AttributedLabelAttachment *)attachment
  380. {
  381. attachment.fontAscent = _fontAscent;
  382. attachment.fontDescent = _fontDescent;
  383. unichar objectReplacementChar = 0xFFFC;
  384. NSString *objectReplacementString = [NSString stringWithCharacters:&objectReplacementChar length:1];
  385. NSMutableAttributedString *attachText = [[NSMutableAttributedString alloc]initWithString:objectReplacementString];
  386. CTRunDelegateCallbacks callbacks;
  387. callbacks.version = kCTRunDelegateVersion1;
  388. callbacks.getAscent = ascentCallback;
  389. callbacks.getDescent = descentCallback;
  390. callbacks.getWidth = widthCallback;
  391. callbacks.dealloc = deallocCallback;
  392. CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (void *)attachment);
  393. NSDictionary *attr = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)delegate,kCTRunDelegateAttributeName, nil];
  394. [attachText setAttributes:attr range:NSMakeRange(0, 1)];
  395. CFRelease(delegate);
  396. [_attachments addObject:attachment];
  397. [self appendAttributedText:attachText];
  398. }
  399. #pragma mark - 设置文本
  400. - (void)setText:(NSString *)text
  401. {
  402. NSAttributedString *attributedText = [self attributedString:text];
  403. [self setAttributedText:attributedText];
  404. }
  405. - (void)setAttributedText:(NSAttributedString *)attributedText
  406. {
  407. _attributedString = [[NSMutableAttributedString alloc]initWithAttributedString:attributedText];
  408. [self cleanAll];
  409. }
  410. - (NSString *)text
  411. {
  412. return [_attributedString string];
  413. }
  414. - (NSAttributedString *)attributedText
  415. {
  416. return [_attributedString copy];
  417. }
  418. #pragma mark - 添加文本
  419. - (void)appendText:(NSString *)text
  420. {
  421. NSAttributedString *attributedText = [self attributedString:text];
  422. [self appendAttributedText:attributedText];
  423. }
  424. - (void)appendAttributedText:(NSAttributedString *)attributedText
  425. {
  426. [_attributedString appendAttributedString:attributedText];
  427. [self resetTextFrame];
  428. }
  429. #pragma mark - 添加图片
  430. - (void)appendImage:(UIImage *)image
  431. {
  432. [self appendImage:image
  433. maxSize:image.size];
  434. }
  435. - (void)appendImage:(UIImage *)image
  436. maxSize:(CGSize)maxSize
  437. {
  438. [self appendImage:image
  439. maxSize:maxSize
  440. margin:UIEdgeInsetsZero];
  441. }
  442. - (void)appendImage:(UIImage *)image
  443. maxSize:(CGSize)maxSize
  444. margin:(UIEdgeInsets)margin
  445. {
  446. [self appendImage:image
  447. maxSize:maxSize
  448. margin:margin
  449. alignment:M80ImageAlignmentBottom];
  450. }
  451. - (void)appendImage:(UIImage *)image
  452. maxSize:(CGSize)maxSize
  453. margin:(UIEdgeInsets)margin
  454. alignment:(M80ImageAlignment)alignment
  455. {
  456. M80AttributedLabelAttachment *attachment = [M80AttributedLabelAttachment attachmentWith:image
  457. margin:margin
  458. alignment:alignment
  459. maxSize:maxSize];
  460. [self appendAttachment:attachment];
  461. }
  462. #pragma mark - 添加UI控件
  463. - (void)appendView:(UIView *)view
  464. {
  465. [self appendView:view
  466. margin:UIEdgeInsetsZero];
  467. }
  468. - (void)appendView:(UIView *)view
  469. margin:(UIEdgeInsets)margin
  470. {
  471. [self appendView:view
  472. margin:margin
  473. alignment:M80ImageAlignmentBottom];
  474. }
  475. - (void)appendView:(UIView *)view
  476. margin:(UIEdgeInsets)margin
  477. alignment:(M80ImageAlignment)alignment
  478. {
  479. M80AttributedLabelAttachment *attachment = [M80AttributedLabelAttachment attachmentWith:view
  480. margin:margin
  481. alignment:alignment
  482. maxSize:CGSizeZero];
  483. [self appendAttachment:attachment];
  484. }
  485. #pragma mark - 添加链接
  486. - (void)addCustomLink:(id)linkData
  487. forRange:(NSRange)range
  488. {
  489. [self addCustomLink:linkData
  490. forRange:range
  491. linkColor:self.linkColor];
  492. }
  493. - (void)addCustomLink:(id)linkData
  494. forRange:(NSRange)range
  495. linkColor:(UIColor *)color
  496. {
  497. M80AttributedLabelURL *url = [M80AttributedLabelURL urlWithLinkData:linkData
  498. range:range
  499. color:color];
  500. [_linkLocations addObject:url];
  501. [self resetTextFrame];
  502. }
  503. #pragma mark - 计算大小
  504. - (CGSize)sizeThatFits:(CGSize)size
  505. {
  506. NSAttributedString *drawString = [self attributedStringForDraw];
  507. if (drawString == nil)
  508. {
  509. return CGSizeZero;
  510. }
  511. CFAttributedStringRef attributedStringRef = (__bridge CFAttributedStringRef)drawString;
  512. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedStringRef);
  513. CFRange range = CFRangeMake(0, 0);
  514. if (_numberOfLines > 0 && framesetter)
  515. {
  516. CGMutablePathRef path = CGPathCreateMutable();
  517. CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
  518. CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
  519. CFArrayRef lines = CTFrameGetLines(frame);
  520. if (nil != lines && CFArrayGetCount(lines) > 0)
  521. {
  522. NSInteger lastVisibleLineIndex = MIN(_numberOfLines, CFArrayGetCount(lines)) - 1;
  523. CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
  524. CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
  525. range = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
  526. }
  527. CFRelease(frame);
  528. CFRelease(path);
  529. }
  530. CFRange fitCFRange = CFRangeMake(0, 0);
  531. CGSize newSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, range, NULL, size, &fitCFRange);
  532. if (framesetter)
  533. {
  534. CFRelease(framesetter);
  535. }
  536. //hack:
  537. //1.需要加上额外的一部分size,有些情况下计算出来的像素点并不是那么精准
  538. //2.iOS7 的 CTFramesetterSuggestFrameSizeWithConstraint s方法比较残,需要多加一部分 height
  539. //3.iOS7 多行中如果首行带有很多空格,会导致返回的 suggestionWidth 远小于真实 width ,那么多行情况下就是用传入的 width
  540. if (newSize.height < _fontHeight * 2) //单行
  541. {
  542. return CGSizeMake(ceilf(newSize.width) + 2.0, ceilf(newSize.height) + 4.0);
  543. }
  544. else
  545. {
  546. return CGSizeMake(size.width, ceilf(newSize.height) + 4.0);
  547. }
  548. }
  549. - (CGSize)intrinsicContentSize
  550. {
  551. return [self sizeThatFits:CGSizeMake(CGRectGetWidth(self.bounds), CGFLOAT_MAX)];
  552. }
  553. #pragma mark - 绘制方法
  554. - (void)drawRect:(CGRect)rect
  555. {
  556. CGContextRef ctx = UIGraphicsGetCurrentContext();
  557. if (ctx == nil)
  558. {
  559. return;
  560. }
  561. CGContextSaveGState(ctx);
  562. CGAffineTransform transform = [self transformForCoreText];
  563. CGContextConcatCTM(ctx, transform);
  564. [self recomputeLinksIfNeeded];
  565. NSAttributedString *drawString = [self attributedStringForDraw];
  566. if (drawString)
  567. {
  568. [self prepareTextFrame:drawString rect:rect];
  569. [self drawHighlightWithRect:rect];
  570. [self drawAttachments];
  571. [self drawShadow:ctx];
  572. [self drawText:drawString
  573. rect:rect
  574. context:ctx];
  575. }
  576. CGContextRestoreGState(ctx);
  577. }
  578. - (void)prepareTextFrame:(NSAttributedString *)string
  579. rect:(CGRect)rect
  580. {
  581. if (_textFrame == nil)
  582. {
  583. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)string);
  584. CGMutablePathRef path = CGPathCreateMutable();
  585. CGPathAddRect(path, nil,rect);
  586. _textFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
  587. CGPathRelease(path);
  588. CFRelease(framesetter);
  589. }
  590. }
  591. - (void)drawHighlightWithRect:(CGRect)rect
  592. {
  593. if (self.touchedLink && self.highlightColor)
  594. {
  595. [self.highlightColor setFill];
  596. NSRange linkRange = self.touchedLink.range;
  597. CFArrayRef lines = CTFrameGetLines(_textFrame);
  598. CFIndex count = CFArrayGetCount(lines);
  599. CGPoint lineOrigins[count];
  600. CTFrameGetLineOrigins(_textFrame, CFRangeMake(0, 0), lineOrigins);
  601. NSInteger numberOfLines = [self numberOfDisplayedLines];
  602. CGContextRef ctx = UIGraphicsGetCurrentContext();
  603. for (CFIndex i = 0; i < numberOfLines; i++)
  604. {
  605. CTLineRef line = CFArrayGetValueAtIndex(lines, i);
  606. CFRange stringRange = CTLineGetStringRange(line);
  607. NSRange lineRange = NSMakeRange(stringRange.location, stringRange.length);
  608. NSRange intersectedRange = NSIntersectionRange(lineRange, linkRange);
  609. if (intersectedRange.length == 0) {
  610. continue;
  611. }
  612. CGRect highlightRect = [self rectForRange:linkRange
  613. inLine:line
  614. lineOrigin:lineOrigins[i]];
  615. highlightRect = CGRectOffset(highlightRect, 0, -rect.origin.y);
  616. if (!CGRectIsEmpty(highlightRect))
  617. {
  618. CGFloat pi = (CGFloat)M_PI;
  619. CGFloat radius = 1.0f;
  620. CGContextMoveToPoint(ctx, highlightRect.origin.x, highlightRect.origin.y + radius);
  621. CGContextAddLineToPoint(ctx, highlightRect.origin.x, highlightRect.origin.y + highlightRect.size.height - radius);
  622. CGContextAddArc(ctx, highlightRect.origin.x + radius, highlightRect.origin.y + highlightRect.size.height - radius,
  623. radius, pi, pi / 2.0f, 1.0f);
  624. CGContextAddLineToPoint(ctx, highlightRect.origin.x + highlightRect.size.width - radius,
  625. highlightRect.origin.y + highlightRect.size.height);
  626. CGContextAddArc(ctx, highlightRect.origin.x + highlightRect.size.width - radius,
  627. highlightRect.origin.y + highlightRect.size.height - radius, radius, pi / 2, 0.0f, 1.0f);
  628. CGContextAddLineToPoint(ctx, highlightRect.origin.x + highlightRect.size.width, highlightRect.origin.y + radius);
  629. CGContextAddArc(ctx, highlightRect.origin.x + highlightRect.size.width - radius, highlightRect.origin.y + radius,
  630. radius, 0.0f, -pi / 2.0f, 1.0f);
  631. CGContextAddLineToPoint(ctx, highlightRect.origin.x + radius, highlightRect.origin.y);
  632. CGContextAddArc(ctx, highlightRect.origin.x + radius, highlightRect.origin.y + radius, radius,
  633. -pi / 2, pi, 1);
  634. CGContextFillPath(ctx);
  635. }
  636. }
  637. }
  638. }
  639. - (void)drawShadow:(CGContextRef)ctx
  640. {
  641. if (self.shadowColor)
  642. {
  643. CGContextSetShadowWithColor(ctx, self.shadowOffset, self.shadowBlur, self.shadowColor.CGColor);
  644. }
  645. }
  646. - (void)drawText:(NSAttributedString *)attributedString
  647. rect:(CGRect)rect
  648. context:(CGContextRef)context
  649. {
  650. if (_textFrame)
  651. {
  652. if (_numberOfLines > 0)
  653. {
  654. CFArrayRef lines = CTFrameGetLines(_textFrame);
  655. NSInteger numberOfLines = [self numberOfDisplayedLines];
  656. CGPoint lineOrigins[numberOfLines];
  657. CTFrameGetLineOrigins(_textFrame, CFRangeMake(0, numberOfLines), lineOrigins);
  658. for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++)
  659. {
  660. CGPoint lineOrigin = lineOrigins[lineIndex];
  661. CGContextSetTextPosition(context, lineOrigin.x, lineOrigin.y);
  662. CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
  663. BOOL shouldDrawLine = YES;
  664. if (lineIndex == numberOfLines - 1 &&
  665. _lineBreakMode == kCTLineBreakByTruncatingTail)
  666. {
  667. //找到最后一行并检查是否需要 truncatingTail
  668. CFRange lastLineRange = CTLineGetStringRange(line);
  669. if (lastLineRange.location + lastLineRange.length < attributedString.length)
  670. {
  671. CTLineTruncationType truncationType = kCTLineTruncationEnd;
  672. NSUInteger truncationAttributePosition = lastLineRange.location + lastLineRange.length - 1;
  673. NSDictionary *tokenAttributes = [attributedString attributesAtIndex:truncationAttributePosition
  674. effectiveRange:NULL];
  675. NSAttributedString *tokenString = [[NSAttributedString alloc] initWithString:M80EllipsesCharacter
  676. attributes:tokenAttributes];
  677. CTLineRef truncationToken = CTLineCreateWithAttributedString((CFAttributedStringRef)tokenString);
  678. NSMutableAttributedString *truncationString = [[attributedString attributedSubstringFromRange:NSMakeRange(lastLineRange.location, lastLineRange.length)] mutableCopy];
  679. if (lastLineRange.length > 0)
  680. {
  681. //移除掉最后一个对象...(其实这个地方有点问题,也有可能需要移除最后 2 个对象,因为 attachment 宽度的关系)
  682. [truncationString deleteCharactersInRange:NSMakeRange(lastLineRange.length - 1, 1)];
  683. }
  684. [truncationString appendAttributedString:tokenString];
  685. CTLineRef truncationLine = CTLineCreateWithAttributedString((CFAttributedStringRef)truncationString);
  686. CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, rect.size.width, truncationType, truncationToken);
  687. if (!truncatedLine)
  688. {
  689. truncatedLine = CFRetain(truncationToken);
  690. }
  691. CFRelease(truncationLine);
  692. CFRelease(truncationToken);
  693. CTLineDraw(truncatedLine, context);
  694. CFRelease(truncatedLine);
  695. shouldDrawLine = NO;
  696. }
  697. }
  698. if(shouldDrawLine)
  699. {
  700. CTLineDraw(line, context);
  701. }
  702. }
  703. }
  704. else
  705. {
  706. CTFrameDraw(_textFrame,context);
  707. }
  708. }
  709. }
  710. - (void)drawAttachments
  711. {
  712. if ([_attachments count] == 0)
  713. {
  714. return;
  715. }
  716. CGContextRef ctx = UIGraphicsGetCurrentContext();
  717. if (ctx == nil)
  718. {
  719. return;
  720. }
  721. CFArrayRef lines = CTFrameGetLines(_textFrame);
  722. CFIndex lineCount = CFArrayGetCount(lines);
  723. CGPoint lineOrigins[lineCount];
  724. CTFrameGetLineOrigins(_textFrame, CFRangeMake(0, 0), lineOrigins);
  725. NSInteger numberOfLines = [self numberOfDisplayedLines];
  726. for (CFIndex i = 0; i < numberOfLines; i++)
  727. {
  728. CTLineRef line = CFArrayGetValueAtIndex(lines, i);
  729. CFArrayRef runs = CTLineGetGlyphRuns(line);
  730. CFIndex runCount = CFArrayGetCount(runs);
  731. CGPoint lineOrigin = lineOrigins[i];
  732. CGFloat lineAscent;
  733. CGFloat lineDescent;
  734. CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, NULL);
  735. CGFloat lineHeight = lineAscent + lineDescent;
  736. CGFloat lineBottomY = lineOrigin.y - lineDescent;
  737. //遍历以找到对应的 attachment 进行绘制
  738. for (CFIndex k = 0; k < runCount; k++)
  739. {
  740. CTRunRef run = CFArrayGetValueAtIndex(runs, k);
  741. NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
  742. CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
  743. if (nil == delegate)
  744. {
  745. continue;
  746. }
  747. M80AttributedLabelAttachment* attributedImage = (M80AttributedLabelAttachment *)CTRunDelegateGetRefCon(delegate);
  748. CGFloat ascent = 0.0f;
  749. CGFloat descent = 0.0f;
  750. CGFloat width = (CGFloat)CTRunGetTypographicBounds(run,
  751. CFRangeMake(0, 0),
  752. &ascent,
  753. &descent,
  754. NULL);
  755. CGFloat imageBoxHeight = [attributedImage boxSize].height;
  756. CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil);
  757. CGFloat imageBoxOriginY = 0.0f;
  758. switch (attributedImage.alignment)
  759. {
  760. case M80ImageAlignmentTop:
  761. imageBoxOriginY = lineBottomY + (lineHeight - imageBoxHeight);
  762. break;
  763. case M80ImageAlignmentCenter:
  764. imageBoxOriginY = lineBottomY + (lineHeight - imageBoxHeight) / 2.0;
  765. break;
  766. case M80ImageAlignmentBottom:
  767. imageBoxOriginY = lineBottomY;
  768. break;
  769. }
  770. CGRect rect = CGRectMake(lineOrigin.x + xOffset, imageBoxOriginY, width, imageBoxHeight);
  771. UIEdgeInsets flippedMargins = attributedImage.margin;
  772. CGFloat top = flippedMargins.top;
  773. flippedMargins.top = flippedMargins.bottom;
  774. flippedMargins.bottom = top;
  775. CGRect attatchmentRect = UIEdgeInsetsInsetRect(rect, flippedMargins);
  776. if (i == numberOfLines - 1 &&
  777. k >= runCount - 2 &&
  778. _lineBreakMode == kCTLineBreakByTruncatingTail)
  779. {
  780. //最后行最后的2个CTRun需要做额外判断
  781. CGFloat attachmentWidth = CGRectGetWidth(attatchmentRect);
  782. const CGFloat kMinEllipsesWidth = attachmentWidth;
  783. if (CGRectGetWidth(self.bounds) - CGRectGetMinX(attatchmentRect) - attachmentWidth < kMinEllipsesWidth)
  784. {
  785. continue;
  786. }
  787. }
  788. id content = attributedImage.content;
  789. if ([content isKindOfClass:[UIImage class]])
  790. {
  791. CGContextDrawImage(ctx, attatchmentRect, ((UIImage *)content).CGImage);
  792. }
  793. else if ([content isKindOfClass:[UIView class]])
  794. {
  795. UIView *view = (UIView *)content;
  796. if (view.superview == nil)
  797. {
  798. [self addSubview:view];
  799. }
  800. CGRect viewFrame = CGRectMake(attatchmentRect.origin.x,
  801. self.bounds.size.height - attatchmentRect.origin.y - attatchmentRect.size.height,
  802. attatchmentRect.size.width,
  803. attatchmentRect.size.height);
  804. [view setFrame:viewFrame];
  805. }
  806. else
  807. {
  808. NSLog(@"Attachment Content Not Supported %@",content);
  809. }
  810. }
  811. }
  812. }
  813. #pragma mark - 点击事件处理
  814. - (BOOL)onLabelClick:(CGPoint)point
  815. {
  816. id linkData = [self linkDataForPoint:point];
  817. if (linkData)
  818. {
  819. if (_delegate && [_delegate respondsToSelector:@selector(m80AttributedLabel:clickedOnLink:)])
  820. {
  821. [_delegate m80AttributedLabel:self clickedOnLink:linkData];
  822. }
  823. else
  824. {
  825. NSURL *url = nil;
  826. if ([linkData isKindOfClass:[NSString class]])
  827. {
  828. url = [NSURL URLWithString:linkData];
  829. }
  830. else if([linkData isKindOfClass:[NSURL class]])
  831. {
  832. url = linkData;
  833. }
  834. if (url)
  835. {
  836. [[UIApplication sharedApplication] openURL:url];
  837. }
  838. }
  839. return YES;
  840. }
  841. return NO;
  842. }
  843. #pragma mark - 链接处理
  844. - (void)recomputeLinksIfNeeded
  845. {
  846. const NSInteger kMinHttpLinkLength = 5;
  847. if (!_autoDetectLinks || _linkDetected)
  848. {
  849. return;
  850. }
  851. NSString *text = [[_attributedString string] copy];
  852. NSUInteger length = [text length];
  853. if (length <= kMinHttpLinkLength)
  854. {
  855. return;
  856. }
  857. BOOL sync = length <= M80MinAsyncDetectLinkLength;
  858. [self computeLink:text
  859. sync:sync];
  860. }
  861. - (void)computeLink:(NSString *)text
  862. sync:(BOOL)sync
  863. {
  864. __weak typeof(self) weakSelf = self;
  865. typedef void (^LinkBlock) (NSArray *);
  866. LinkBlock block = ^(NSArray *links)
  867. {
  868. weakSelf.linkDetected = YES;
  869. if ([links count])
  870. {
  871. for (M80AttributedLabelURL *link in links)
  872. {
  873. [weakSelf addAutoDetectedLink:link];
  874. }
  875. [weakSelf resetTextFrame];
  876. }
  877. };
  878. if (sync)
  879. {
  880. _ignoreRedraw = YES;
  881. NSArray *links = [M80AttributedLabelURL detectLinks:text];
  882. block(links);
  883. _ignoreRedraw = NO;
  884. }
  885. else
  886. {
  887. dispatch_async(get_m80_attributed_label_parse_queue(), ^{
  888. NSArray *links = [M80AttributedLabelURL detectLinks:text];
  889. dispatch_async(dispatch_get_main_queue(), ^{
  890. NSString *plainText = [[weakSelf attributedString] string];
  891. if ([plainText isEqualToString:text])
  892. {
  893. block(links);
  894. }
  895. });
  896. });
  897. }
  898. }
  899. - (void)addAutoDetectedLink:(M80AttributedLabelURL *)link
  900. {
  901. NSRange range = link.range;
  902. for (M80AttributedLabelURL *url in _linkLocations)
  903. {
  904. if (NSIntersectionRange(range, url.range).length != 0)
  905. {
  906. return;
  907. }
  908. }
  909. [self addCustomLink:link.linkData
  910. forRange:link.range];
  911. }
  912. #pragma mark - 点击事件相应
  913. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  914. {
  915. if (self.touchedLink == nil)
  916. {
  917. UITouch *touch = [touches anyObject];
  918. CGPoint point = [touch locationInView:self];
  919. self.touchedLink = [self urlForPoint:point];
  920. }
  921. if (self.touchedLink)
  922. {
  923. [self setNeedsDisplay];
  924. }
  925. else
  926. {
  927. [super touchesBegan:touches withEvent:event];
  928. }
  929. }
  930. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  931. {
  932. [super touchesMoved:touches withEvent:event];
  933. UITouch *touch = [touches anyObject];
  934. CGPoint point = [touch locationInView:self];
  935. M80AttributedLabelURL *touchedLink = [self urlForPoint:point];
  936. if (self.touchedLink != touchedLink)
  937. {
  938. self.touchedLink = touchedLink;
  939. [self setNeedsDisplay];
  940. }
  941. }
  942. - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
  943. {
  944. [super touchesCancelled:touches withEvent:event];
  945. if (self.touchedLink)
  946. {
  947. self.touchedLink = nil;
  948. [self setNeedsDisplay];
  949. }
  950. }
  951. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  952. {
  953. UITouch *touch = [touches anyObject];
  954. CGPoint point = [touch locationInView:self];
  955. if(![self onLabelClick:point])
  956. {
  957. [super touchesEnded:touches withEvent:event];
  958. }
  959. if (self.touchedLink)
  960. {
  961. self.touchedLink = nil;
  962. [self setNeedsDisplay];
  963. }
  964. }
  965. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
  966. {
  967. M80AttributedLabelURL *touchedLink = [self urlForPoint:point];
  968. if (touchedLink == nil)
  969. {
  970. NSArray *subViews = [self subviews];
  971. for (UIView *view in subViews)
  972. {
  973. CGPoint hitPoint = [view convertPoint:point
  974. fromView:self];
  975. UIView *hitTestView = [view hitTest:hitPoint
  976. withEvent:event];
  977. if (hitTestView)
  978. {
  979. return hitTestView;
  980. }
  981. }
  982. return nil;
  983. }
  984. else
  985. {
  986. return self;
  987. }
  988. }
  989. #pragma mark - 设置自定义的连接检测block
  990. + (void)setCustomDetectMethod:(M80CustomDetectLinkBlock)block
  991. {
  992. [M80AttributedLabelURL setCustomDetectMethod:block];
  993. }
  994. @end