PageRenderTime 50ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/coreTextDemo/RxLabel/RxLabel.m

https://gitlab.com/Mr.Tomato/RxLabel
Objective C | 459 lines | 300 code | 80 blank | 79 comment | 28 complexity | d28332747f848dd2a2a813ba692a4938 MD5 | raw file
  1. //
  2. // RxLabel.m
  3. // coreTextDemo
  4. //
  5. // Created by roxasora on 15/10/8.
  6. // Copyright © 2015年 roxasora. All rights reserved.
  7. //
  8. #import "RxLabel.h"
  9. #import <CoreText/CoreText.h>
  10. #import "RxTextLinkTapView.h"
  11. #define UIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
  12. #define RxUrlRegular @"((http[s]{0,1}|ftp)://[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|(www.[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)"
  13. #define RxTopicRegular @"#[^#]+#"
  14. #define rxHighlightTextTypeUrl @"url"
  15. #define subviewsTag_linkTapViews -333
  16. #define lineHeight_correction 3 //correct the line height
  17. @interface RxLabel ()<RxTextLinkTapViewDelegate>
  18. @end
  19. @implementation RxLabel
  20. -(id)initWithFrame:(CGRect)frame{
  21. self = [super initWithFrame:frame];
  22. if (self) {
  23. _font = [UIFont systemFontOfSize:16];
  24. _textColor = UIColorFromRGB(0X333333);
  25. _linkButtonColor = UIColorFromRGB(0X2081ef);
  26. _linespacing = 0;
  27. _textAlignment = NSTextAlignmentLeft;
  28. self.backgroundColor = [UIColor clearColor];
  29. }
  30. return self;
  31. }
  32. -(void)setFrame:(CGRect)frame{
  33. [super setFrame:frame];
  34. [self setNeedsDisplay];
  35. }
  36. -(void)setText:(NSString *)text{
  37. _text = text;
  38. // [self drawRect:self.bounds];
  39. [self setNeedsDisplay];
  40. }
  41. -(void)setFont:(UIFont *)font{
  42. _font = font;
  43. [self setNeedsDisplay];
  44. }
  45. -(void)setTextColor:(UIColor *)textColor{
  46. _textColor = textColor;
  47. [self setNeedsDisplay];
  48. }
  49. -(void)setTextAlignment:(NSTextAlignment)textAlignment{
  50. _textAlignment = textAlignment;
  51. [self setNeedsDisplay];
  52. }
  53. -(void)setLinkButtonColor:(UIColor *)linkButtonColor{
  54. _linkButtonColor = linkButtonColor;
  55. for (UIView* subview in self.subviews) {
  56. if (subview.tag == NSIntegerMin) {
  57. RxTextLinkTapView* buttonView = nil;
  58. buttonView = (RxTextLinkTapView*)subview;
  59. if (buttonView.type == RxTextLinkTapViewTypeDefault) {
  60. buttonView.backgroundColor = linkButtonColor;
  61. }
  62. }
  63. }
  64. }
  65. -(void)setlinespacing:(NSInteger)linespacing{
  66. _linespacing = linespacing;
  67. [self setNeedsDisplay];
  68. }
  69. -(void)setCustomUrlArray:(NSMutableArray *)customUrlArray{
  70. _customUrlArray = customUrlArray;
  71. [self setNeedsDisplay];
  72. }
  73. static CTTextAlignment CTTextAlignmentFromNSTextAlignment(NSTextAlignment alignment){
  74. switch (alignment) {
  75. case NSTextAlignmentCenter: return kCTCenterTextAlignment;
  76. case NSTextAlignmentLeft: return kCTLeftTextAlignment;
  77. case NSTextAlignmentRight: return kCTRightTextAlignment;
  78. default: return kCTNaturalTextAlignment;
  79. }
  80. }
  81. #pragma mark - url replace run delegate
  82. static CGFloat ascentCallback(void *ref){
  83. //!the height must fit the fontsize of titleView
  84. return [(__bridge UIFont*)ref pointSize] + 2;
  85. return [(NSNumber*)[(__bridge NSDictionary*)ref objectForKey:@"height"] floatValue];
  86. }
  87. static CGFloat descentCallback(void *ref){
  88. return 0;
  89. }
  90. static CGFloat widthCallback(void* ref){
  91. return 70.0;
  92. return [(NSNumber*)[(__bridge NSDictionary*)ref objectForKey:@"width"] floatValue];
  93. }
  94. #pragma mark - draw rect
  95. // Only override drawRect: if you perform custom drawing.
  96. // An empty implementation adversely affects performance during animation.
  97. - (void)drawRect:(CGRect)rect {
  98. // Drawing code
  99. [super drawRect:rect];
  100. CGContextRef context = UIGraphicsGetCurrentContext();
  101. CGContextClearRect(context, self.bounds);
  102. if (self.backgroundColor) {
  103. CGContextSaveGState(context);
  104. CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
  105. CGContextFillRect(context, self.bounds);
  106. CGContextRestoreGState(context);
  107. }
  108. //translate the coordinate system to normal
  109. CGContextSetTextMatrix(context, CGAffineTransformIdentity);
  110. CGContextTranslateCTM(context, 0, self.bounds.size.height);
  111. CGContextScaleCTM(context, 1.0, -1.0);
  112. //create the draw path
  113. CGMutablePathRef path = CGPathCreateMutable();
  114. //挪动path的bound,避免(ಥ_ಥ) 这样的符号会画不出来
  115. //move the bound of path,or some words like (ಥ_ಥ) won't be drawn
  116. CGRect pathRect = self.bounds;
  117. pathRect.size.height += 5;
  118. pathRect.origin.y -= 5;
  119. CGPathAddRect(path, NULL, pathRect);
  120. //set line height font color and break mode
  121. CTFontRef fontRef = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL);
  122. // CGFloat minLineHeight = self.font.pointSize + lineHeight_correction,
  123. // maxLineHeight = minLineHeight,
  124. CGFloat linespacing = self.linespacing;
  125. CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping;
  126. CTTextAlignment alignment = CTTextAlignmentFromNSTextAlignment(self.textAlignment);
  127. CTParagraphStyleRef style = CTParagraphStyleCreate((CTParagraphStyleSetting[4]){
  128. {kCTParagraphStyleSpecifierAlignment,sizeof(alignment),&alignment},
  129. // {kCTParagraphStyleSpecifierMinimumLineHeight,sizeof(minLineHeight),&minLineHeight},
  130. // {kCTParagraphStyleSpecifierMaximumLineHeight,sizeof(maxLineHeight),&maxLineHeight},
  131. {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(linespacing),&linespacing},
  132. {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(linespacing),&linespacing},
  133. {kCTParagraphStyleSpecifierLineBreakMode,sizeof(lineBreakMode),&lineBreakMode}
  134. }, 4);
  135. NSDictionary* initAttrbutes = @{
  136. (NSString*)kCTFontAttributeName: (__bridge id)fontRef,
  137. (NSString*)kCTForegroundColorAttributeName:(id)self.textColor.CGColor,
  138. (NSString*)kCTParagraphStyleAttributeName:(id)style
  139. };
  140. //先从self text 中过滤掉 url ,将其保存在array中
  141. //filter the url string from origin text and generate the urlArray and the filtered text string
  142. /**
  143. @[
  144. @{
  145. @"range":@(m,n),
  146. @"urlStr":@"http://dsadd"
  147. }
  148. ]
  149. */
  150. NSMutableArray* urlArray = [NSMutableArray array];
  151. NSString* filteredText = [[NSString alloc] init];
  152. [RxLabel filtUrlWithOriginText:self.text urlArray:urlArray filteredText:&filteredText];
  153. //init the attributed string
  154. NSMutableAttributedString* attrStr = [[NSMutableAttributedString alloc] initWithString:filteredText
  155. attributes:initAttrbutes];
  156. //add url replaced run one by one with urlArray
  157. for (NSDictionary* urlItem in urlArray) {
  158. //init run callbacks
  159. CTRunDelegateCallbacks callbacks;
  160. memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
  161. callbacks.version = kCTRunDelegateVersion1;
  162. callbacks.getAscent = ascentCallback;
  163. callbacks.getDescent = descentCallback;
  164. callbacks.getWidth = widthCallback;
  165. CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void*)(self.font));
  166. NSRange range = [[urlItem objectForKey:@"range"] rangeValue];
  167. NSString* urlStr = [urlItem objectForKey:@"urlStr"];
  168. CFAttributedStringSetAttributes((CFMutableAttributedStringRef)attrStr, CFRangeMake(range.location, range.length), (CFDictionaryRef)@{
  169. (NSString*)kCTRunDelegateAttributeName:(__bridge id)delegate,
  170. @"url":urlStr
  171. }, NO);
  172. CFRelease(delegate);
  173. }
  174. CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
  175. CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attrStr.length), path, NULL);
  176. //clear link tap views and create new link tap view
  177. for (UIView* subview in self.subviews) {
  178. if (subview.tag == NSIntegerMin) {
  179. [subview removeFromSuperview];
  180. }
  181. }
  182. //get lines in frame
  183. NSArray* lines = (NSArray*)CTFrameGetLines(frame);
  184. CFIndex lineCount = [lines count];
  185. //get origin point of each line
  186. CGPoint origins[lineCount];
  187. CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
  188. for (CFIndex index = 0; index < lineCount; index++) {
  189. //get line ref of line
  190. CTLineRef line = CFArrayGetValueAtIndex((CFArrayRef)lines, index);
  191. //get run
  192. CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
  193. CFIndex glyphCount = CFArrayGetCount(glyphRuns);
  194. for (int i = 0; i < glyphCount; i++) {
  195. CTRunRef run = CFArrayGetValueAtIndex(glyphRuns, i);
  196. NSDictionary* attrbutes = (NSDictionary*)CTRunGetAttributes(run);
  197. //create hover frame
  198. if ([attrbutes objectForKey:@"url"]) {
  199. CGRect runBounds;
  200. CGFloat ascent;
  201. CGFloat descent;
  202. runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
  203. runBounds.size.height = ascent + descent;
  204. //!make sure you've add the origin of the line, or your alignment will not work on url replace runs
  205. runBounds.origin.x = origins[index].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
  206. runBounds.origin.y = self.frame.size.height - origins[index].y - runBounds.size.height;
  207. //加上之前给 path 挪动位置时的修正
  208. //add correction of move the path
  209. runBounds.origin.y += 5;
  210. #ifdef RXDEBUG
  211. UIView* randomView = [[UIView alloc] initWithFrame:runBounds];
  212. randomView.backgroundColor = [UIColor colorWithRed:arc4random()%255/255.0 green:arc4random()%255/255.0 blue:arc4random()%255/255.0 alpha:0.2];
  213. [self addSubview:randomView];
  214. #endif
  215. NSString* urlStr = attrbutes[@"url"];
  216. RxTextLinkTapView* linkButtonView = [self linkButtonViewWithFrame:runBounds UrlStr:urlStr];
  217. [self addSubview:linkButtonView];
  218. }
  219. }
  220. }
  221. CTFrameDraw(frame, context);
  222. CFRelease(frame);
  223. CFRelease(frameSetter);
  224. CFRelease(path);
  225. }
  226. -(void)sizeToFit{
  227. [super sizeToFit];
  228. CGFloat height = [RxLabel heightForText:self.text width:self.bounds.size.width font:self.font linespacing:self.linespacing];
  229. CGRect frame = self.frame;
  230. frame.size.height = height;
  231. self.frame = frame;
  232. }
  233. #pragma mark create link replace button with url
  234. -(RxTextLinkTapView*)linkButtonViewWithFrame:(CGRect)frame UrlStr:(NSString*)urlStr{
  235. RxTextLinkTapView* buttonView = [[RxTextLinkTapView alloc] initWithFrame:frame
  236. urlStr:urlStr
  237. font:self.font
  238. linespacing:self.linespacing];
  239. buttonView.tag = NSIntegerMin;
  240. buttonView.backgroundColor = self.linkButtonColor;
  241. buttonView.title = @"网页";
  242. buttonView.delegate = self;
  243. //handle custom url array
  244. for (NSDictionary* item in self.customUrlArray) {
  245. NSString* scheme = item[@"scheme"];
  246. //when match
  247. if ([urlStr rangeOfString:scheme].location != NSNotFound) {
  248. buttonView.type = RxTextLinkTapViewTypeCustom;
  249. buttonView.backgroundColor = UIColorFromRGB([item[@"color"] integerValue]);
  250. buttonView.title = item[@"title"];
  251. }
  252. }
  253. return buttonView;
  254. }
  255. #pragma mark - filter url and generate display text and url array
  256. +(void)filtUrlWithOriginText:(NSString *)originText urlArray:(NSMutableArray *)urlArray filteredText:(NSString *__autoreleasing *)filterText{
  257. *filterText = [NSString stringWithString:originText];
  258. NSArray* urlMatches = [[NSRegularExpression regularExpressionWithPattern:RxUrlRegular
  259. options:NSRegularExpressionDotMatchesLineSeparators error:nil]
  260. matchesInString:originText
  261. options:0
  262. range:NSMakeRange(0, originText.length)];
  263. //range 的偏移量,每次replace之后,下次循环中,要加上这个偏移量
  264. // NSLog(@"origin text %@ matched%@",originText,urlMatches);
  265. NSInteger rangeOffset = 0;
  266. for (NSTextCheckingResult* match in urlMatches) {
  267. NSRange range = match.range;
  268. NSString* urlStr = [originText substringWithRange:range];
  269. range.location += rangeOffset;
  270. rangeOffset -= (range.length - 1);
  271. unichar objectReplacementChar = 0xFFFC;
  272. NSString * replaceContent = [NSString stringWithCharacters:&objectReplacementChar length:1];
  273. *filterText = [*filterText stringByReplacingCharactersInRange:range withString:replaceContent];
  274. range.length = 1;
  275. [urlArray addObject:@{
  276. @"range":[NSValue valueWithRange:range],
  277. @"urlStr":urlStr
  278. }];
  279. }
  280. }
  281. #pragma mark - get height for particular configs
  282. +(CGFloat)heightForText:(NSString *)text width:(CGFloat)width font:(UIFont *)font linespacing:(CGFloat)linespacing{
  283. CGFloat height = 0;
  284. CGMutablePathRef path = CGPathCreateMutable();
  285. CGPathAddRect(path, NULL, CGRectMake(0, 0, width, 9999));
  286. //set line height font color and break mode
  287. CTFontRef fontRef = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
  288. // CGFloat minLineHeight = font.pointSize + lineHeight_correction,
  289. // maxLineHeight = minLineHeight;
  290. CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping;
  291. CTTextAlignment alignment = kCTLeftTextAlignment;
  292. CTParagraphStyleRef style = CTParagraphStyleCreate((CTParagraphStyleSetting[4]){
  293. {kCTParagraphStyleSpecifierAlignment,sizeof(alignment),&alignment},
  294. // {kCTParagraphStyleSpecifierMinimumLineHeight,sizeof(minLineHeight),&minLineHeight},
  295. // {kCTParagraphStyleSpecifierMaximumLineHeight,sizeof(maxLineHeight),&maxLineHeight},
  296. {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(linespacing),&linespacing},
  297. {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(linespacing),&linespacing},
  298. {kCTParagraphStyleSpecifierLineBreakMode,sizeof(lineBreakMode),&lineBreakMode}
  299. }, 4);
  300. NSDictionary* initAttrbutes = @{
  301. (NSString*)kCTFontAttributeName: (__bridge id)fontRef,
  302. (NSString*)kCTParagraphStyleAttributeName:(id)style
  303. };
  304. NSMutableArray* urlArray = [NSMutableArray array];
  305. NSString* filteredText = [[NSString alloc] init];
  306. [RxLabel filtUrlWithOriginText:text urlArray:urlArray filteredText:&filteredText];
  307. //create the initial attributed string
  308. NSMutableAttributedString* attrStr = [[NSMutableAttributedString alloc] initWithString:filteredText
  309. attributes:initAttrbutes];
  310. for (NSDictionary* urlItem in urlArray) {
  311. //init run callbacks
  312. CTRunDelegateCallbacks callbacks;
  313. memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
  314. callbacks.version = kCTRunDelegateVersion1;
  315. callbacks.getAscent = ascentCallback;
  316. callbacks.getDescent = descentCallback;
  317. callbacks.getWidth = widthCallback;
  318. CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void*)(font));
  319. NSRange range = [[urlItem objectForKey:@"range"] rangeValue];
  320. NSString* urlStr = [urlItem objectForKey:@"urlStr"];
  321. CFAttributedStringSetAttributes((CFMutableAttributedStringRef)attrStr, CFRangeMake(range.location, range.length), (CFDictionaryRef)@{
  322. (NSString*)kCTRunDelegateAttributeName:(__bridge id)delegate,
  323. @"url":urlStr
  324. }, NO);
  325. CFRelease(delegate);
  326. }
  327. CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
  328. CGSize restrictSize = CGSizeMake(width, 10000);
  329. CGSize coreTestSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, restrictSize, nil);
  330. // NSLog(@"calcuted size %@",[NSValue valueWithCGSize:coreTestSize]);
  331. height = coreTestSize.height;
  332. height += 5;
  333. return height;
  334. // CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attrStr.length), path, NULL);
  335. //
  336. // //get lines in frame
  337. // NSArray* lines = (NSArray*)CTFrameGetLines(frame);
  338. // CFIndex lineCount = [lines count];
  339. //
  340. // //get origin point of each line
  341. // CGPoint origins[lineCount];
  342. // CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
  343. //
  344. // CGFloat colHeight = 9999;
  345. //
  346. // height = 9999 - origins[lines.count - 1].y;
  347. //
  348. // //add down correction
  349. // height += 6;
  350. // height = ceilf(height);
  351. //
  352. // CFRelease(frameSetter);
  353. //
  354. // return height;
  355. }
  356. #pragma mark - RxTextLinkTapView delegate
  357. -(void)RxTextLinkTapView:(RxTextLinkTapView *)linkTapView didDetectTapWithUrlStr:(NSString *)urlStr{
  358. // NSLog(@"link tapped !! %@",urlStr);
  359. if ([self.delegate respondsToSelector:@selector(RxLabel:didDetectedTapLinkWithUrlStr:)]) {
  360. [self.delegate RxLabel:self didDetectedTapLinkWithUrlStr:urlStr];
  361. }
  362. }
  363. -(void)RxTextLinkTapView:(RxTextLinkTapView *)linkTapView didBeginHighlightedWithUrlStr:(NSString *)urlStr{
  364. // [self setOtherLinkTapViewHightlighted:YES withUrlStr:urlStr];
  365. }
  366. -(void)RxTextLinkTapView:(RxTextLinkTapView *)linkTapView didEndHighlightedWithUrlStr:(NSString *)urlStr{
  367. // [self setOtherLinkTapViewHightlighted:NO withUrlStr:urlStr];
  368. }
  369. -(void)setOtherLinkTapViewHightlighted:(BOOL)hightlighted withUrlStr:(NSString*)urlStr{
  370. for (UIView* subview in self.subviews) {
  371. if (subview.tag == NSIntegerMin) {
  372. RxTextLinkTapView* lineTapView = (RxTextLinkTapView*)subview;
  373. if ([lineTapView.urlStr isEqualToString:urlStr] && lineTapView.highlighted != hightlighted) {
  374. [lineTapView setHighlighted:hightlighted];
  375. }
  376. }
  377. }
  378. }
  379. @end