#import <UIKit/UIKit.h> typedef void(^LinkTap)(NSRange range, NSURL* url); @interface LinkLabel : UIView @property (nonatomic, copy) NSString* text; @property (nonatomic, strong) UIColor* textColor; @property (nonatomic, assign) CGFloat fontSize; @property (nonatomic, copy) LinkTap tapLink; - (void)addLinkRange:(NSRange)range withUrl:(NSURL *)url; @end
#import "LinkLabel.h" #import <CoreText/CoreText.h> @interface LinkLabel () { CTFrameRef _textFrame; NSMutableArray* _lineRects; NSMutableDictionary* _linkDic;// 存储链接文本及其range位置 } @end @implementation LinkLabel - (instancetype)init { self = [super init]; if (self) { [self commontInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commontInit]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self commontInit]; } return self; } - (void)commontInit { UIGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(userTapGesture:)]; [self addGestureRecognizer:tapRecognizer]; self.userInteractionEnabled = YES; _lineRects = [NSMutableArray array]; _linkDic = [NSMutableDictionary dictionary]; } - (void)addLinkRange:(NSRange)range withUrl:(NSURL *)url { [_linkDic setObject:url forKey:[NSValue valueWithRange:range]]; } // 正则表达式匹配超链接文本 - (void)matchLinkString:(NSString *)string { NSError *error; NSString *regulaStr = @"((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\\.\\-~!@#$%^&*+?:_/=<>]*)?)"; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regulaStr options:NSRegularExpressionCaseInsensitive error:&error]; NSArray *arrayOfAllMatches = [regex matchesInString:string options:0 range:NSMakeRange(0, [string length])]; for (NSTextCheckingResult *match in arrayOfAllMatches) { NSString* linkStrForMatch = [string substringWithRange:match.range]; [_linkDic setObject:[NSURL URLWithString:linkStrForMatch] forKey:[NSValue valueWithRange:match.range]]; } } - (void)setText:(NSString *)text { _text = text; [self matchLinkString:text]; } - (void)drawRect:(CGRect)rect { [super drawRect:rect]; NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:_text]; UIFont* font = [UIFont systemFontOfSize:_fontSize]; [attString addAttribute:(id)kCTFontAttributeName value:font range:NSMakeRange(0, attString.length)]; if (_linkDic.count) { [_linkDic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { NSRange range = [key rangeValue]; [attString addAttribute:(id)kCTForegroundColorAttributeName value:(id)[UIColor blueColor].CGColor range:range]; }]; } CGContextRef context = UIGraphicsGetCurrentContext(); // 翻转坐标系 CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, self.bounds); CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]), path, NULL); CTFrameDraw(frame, context); // CFRelease(frame); CFArrayRef lines = CTFrameGetLines(frame); CFIndex count = CFArrayGetCount(lines); // 获得每一行的 origin 坐标 CGPoint origins[count]; CTFrameGetLineOrigins(frame, CFRangeMake(0,0), origins); // 翻转坐标系 CGAffineTransform transform = CGAffineTransformMakeTranslation(0, self.bounds.size.height); transform = CGAffineTransformScale(transform, 1.f, -1.f); for (int i = 0; i < count; i++) { CGPoint linePoint = origins[i]; CTLineRef line = CFArrayGetValueAtIndex(lines, i); // 获得每一行的 CGRect 信息 CGRect flippedRect = [self getLineBounds:line point:linePoint]; CGRect rect = CGRectApplyAffineTransform(flippedRect, transform); [_lineRects addObject:[NSValue valueWithCGRect:rect]]; } _textFrame = frame; CFRelease(path); CFRelease(framesetter); } - (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point { CGFloat ascent = 0.0f; CGFloat descent = 0.0f; CGFloat leading = 0.0f; //ascent是文本上线,descent是文本下线,leading是文本起始线 CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading); CGFloat height = ascent + descent; return CGRectMake(point.x, point.y - descent, width, height); } - (void)userTapGesture:(UIGestureRecognizer *)recognizer { CGPoint point = [recognizer locationInView:self]; CFArrayRef lines = CTFrameGetLines(_textFrame); CFIndex count = CFArrayGetCount(lines); for (int i = 0; i < count; i ++) { CTLineRef line = CFArrayGetValueAtIndex(lines, i); CGRect rect = [_lineRects[i] CGRectValue]; if (CGRectContainsPoint(rect, point)) { // 将点击的坐标转换成相对于当前行的坐标 CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect)); // 获得当前点击坐标对应的字符串偏移 // 点击坐标最近的光标的最近字符的index,点击字符前半部分会定位到上一个字符的index,点击后半部分才得到正确的index CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint); CGFloat glyphStart; // 根据index获取对应字符据行初始位置距离 CTLineGetOffsetForStringIndex(line, idx, &glyphStart); if (relativePoint.x < glyphStart && idx) { --idx; } [self matchAndOpenLinkUrl:idx]; break; } } } - (void)matchAndOpenLinkUrl:(CFIndex)index { [_linkDic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { NSRange range = [key rangeValue]; if (NSLocationInRange(index, range)) { NSURL* url = (NSURL *)obj; if (self.tapLink) { self.tapLink(range, url); } *stop = YES; } }]; } @end
xib上使用例子:
@property (weak, nonatomic) IBOutlet LinkLabel *linkLabel;
_linkLabel.text = @"陷阵@86之志有死无生http://ddd.edu,一点寒芒先到,随后枪出如龙。纵使敌众我寡,末将亦能万军从中取敌将首级www.baidu.com"; _linkLabel.fontSize = 17; [_linkLabel addLinkRange:NSMakeRange(2, 3) withUrl:[NSURL URLWithString:@"www.google.com"]]; _linkLabel.tapLink = ^(NSRange range, NSURL* url) { NSLog(@"url:%@", url.absoluteString); };