iOS 富文本 url实现点击

说说和聊天界面,会要求把链接,@,##等,特殊的字符串能点击,可能它涉及到CoreText层,自己写起来比较麻烦,平常忙着做项目,也没时间去研究它,都用的第三方,最近项目不紧,自己研究了一下。写篇文章,加深一下印象

这个工程主要创建4个类,MainViewController类属于c层,MainCell和RichLabel属于view层,RichLink属于model层,这四个类中MainViewController,MainCell,RichLink这三个不用看,我也没注释,就是一些简单的创建UILabel控件的代码,主要看RichLabel

下面是MainViewController.m里面的代码,.h没代码就不写出来了

#import "MainViewController.h"

#import "MainCell.h"

@interface MainViewController ()

/** 注释 */

@property (nonatomic,strong) NSArray *dataArr;

@end

@implementation MainViewController

- (void)viewDidLoad {

[super viewDidLoad];

_dataArr = @[@"【国防部:中国首艘国产航母舾装工作进展顺利】中国国防部31号表示,中国首艘航母进展顺利,目前正进行系统设备调试和舾装施工,并将全面开展系泊试验。系泊试验后还将进行海上试验,以及交付部队阶段,相信整个后续工作,大概需时至少两年。 Via凤凰早班车",

@"特朗普将捐100万美元帮助飓风灾民渡过难关 2日再访灾区】美国白宫发言人桑德斯8月31日透露,总统特朗普将以个人名义捐出100万美元,帮助受飓风“哈维”影响的得克萨斯州和路易斯安那州灾民救灾。此外,特朗普夫妇还将于9月2日再次前往受灾地区视察。http://t.cn/RNfnGvO",

@"建筑工人在商场弹起优美钢琴曲 网友:隐士高人 铁汉柔情】“可能为了生活,需要放下梦想”,30日,香港曹先生在商场里拍下这样一幕:一位身着工地制服的建筑工在认真弹奏钢琴。许多网友被这美妙的音乐和投入的弹奏所感动~@新京报 http://t.cn/RN5w8vc",

@"【出租车驾驶员从业资格考试将公开题库 两考合一】交通运输部将对出租汽车驾驶员资格考试进行改革。交通运输部运输服务司副司长王绣春表示,要对出租车驾驶员从业资格考试进一步优化完善,将实行全国公共科目考试和区域科目考试“两考合一”,优化题库试题、公开考试题库,强化便民利民服务。 ...全文: http://m.weibo.cn/1730243272/4147093503116821",

@"【“十一”黄金周国内游将达6.5亿人次 又能用“朋友圈”环游世界了呢[doge]】《2017十一黄金周旅游趋势预测报告》称,今年“十一”将有6.5亿人次国内游、超600万人出境游。专家建议,尽量错峰出游。看“朋友圈”环游世界的时候又到了...http://t.cn/RNGelZ2 “十一”你选择在家还是出去玩?",

@"#助威国足战在一起# 【郜林点球破门 中国1-0乌兹别克斯坦】中国队1:0战胜乌兹别克斯坦,武汉体育中心五万人赛后大合唱《怒放的生命》,教练@米卢、球员@郜林 等纷纷微博发声,祝贺国足,中国赢下去! http://t.cn/RNqBrpG",

@"#荐书#今日推荐:《理解人性》。对人类来说,最难的是认识自己和改变自己。在书中,作者运用个性心理学的原理,对人的性格进行了科学的剖析,着重强调人的社会性和社会感,强调个人的人生观和价值观在形成性格的过程中所起的作用。 http://t.cn/RNqQjdK"];

}

#pragma mark - Table view data source

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

return _dataArr.count;

}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{

return [MainCell getCellHeight:_dataArr[indexPath.row]];

}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

static NSString *identifier = @"MainCell";

MainCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];

if (cell == nil) {

cell = [[MainCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];

cell.selectionStyle = UITableViewCellSelectionStyleNone;

}

cell.content = _dataArr[indexPath.row];

return cell;

}

@end

下面是cell.h文件里面的代码

#import<UIKit/UIKit.h>

@class RichLabel;

@interface MainCell : UITableViewCell

@property (strong, nonatomic) RichLabel *contentLb;

@property (nonatomic, copy)NSString *content;

+ (CGFloat)getCellHeight:(NSString *)content;

@end

下面是cell.m文件里面的代码

#import "MainCell.h"

#import "RichLabel.h"

#define kEdge 15

@implementation MainCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{

self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];

if (self) {

self.contentLb = [[RichLabel alloc] initWithFrame:CGRectZero];

self.contentLb.userInteractionEnabled = YES;

self.contentLb.numberOfLines = 0;

self.contentLb.lineBreakMode = NSLineBreakByWordWrapping;

self.contentLb.font = kFontSize;

[self.contentView addSubview:self.contentLb];

}

return self;

}

+(CGSize)sizeOfmyText:(NSString *)text font:(UIFont*)myfont width:(CGFloat)mywidth lineSpacing:(CGFloat)lineSpacing

{

if([text isEqual:[NSNull null]] || text==nil || [text isEqualToString:@""])

return CGSizeMake(0.0f, 0.0f);

NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc]init];

paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;

if (lineSpacing > 0) {

paragraphStyle.lineSpacing = lineSpacing;

}

NSDictionary *attributes = @{NSFontAttributeName:myfont, NSParagraphStyleAttributeName:paragraphStyle.copy};

CGSize labelsize = [text boundingRectWithSize:CGSizeMake(mywidth, 2000) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size;

return labelsize;

}

- (void)setContent:(NSString *)content{

_content = content;

self.contentLb.content = content;

CGFloat w = self.contentView.frame.size.width - 2*kEdge;

self.contentLb.frame = CGRectMake(kEdge, 0, w, [MainCell sizeOfmyText:content font:kFontSize width:w lineSpacing:0].height);

}

+ (CGFloat)getCellHeight:(NSString *)content{

return [MainCell sizeOfmyText:content font:kFontSize width:[UIScreen mainScreen].bounds.size.width - 2*kEdge lineSpacing:0].height;

}

@end

下面是RichLink.h的代码,.m没代码,就不写出来了

#import<Foundation/Foundation.h>

@interface RichLink : NSObject

@property (nonatomic, copy) NSString *linkStr;

@property (nonatomic, assign) NSRange range;

@end

RichLabel.h代码,没什么好解释的

#import <UIKit/UIKit.h>

#define kFontSize [UIFont systemFontOfSize:14]

@interface RichLabel : UILabel

@property (nonatomic, copy)NSString *content;

@end

上面的不用看,上面写那么多,就为了方便你粘贴复制到你工程,,这才本文重点,RichLabel.m代码

#import "RichLabel.h"

#import <CoreText/CoreText.h>

#import "RichLink.h"

@implementation RichLabel

- (void)setContent:(NSString *)content{    

_content = content;    

self.attributedText = [self matchesAttributedString:content];

}

#pragma mark 处理链接颜色

- (NSAttributedString *)matchesAttributedString:(NSString *)str{    

NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:str];    

for (NSTextCheckingResult *match in self.matchesArr)    {        

[attrStr addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:match.range];    

}        

[attrStr addAttribute:NSFontAttributeName value:kFontSize range:NSMakeRange(0, attrStr.length)];    

return attrStr;

}

//通过正则表达式匹配出来的特殊文本放到一个数组

- (NSArray *)matchesArr{    

NSError *error;    

NSString *urlPattern = @"((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\\.\\-~!@#$%^&*+?:_/=<>]*)?)";//匹配url   

 NSString *emotionPattern = @"\\[[0-9a-zA-Z\\u4e00-\\u9fa5]+\\]";//匹配自定义表情 带[]    

NSString *atPattern = @"@[0-9a-zA-Z\\u4e00-\\u9fa5]+";//匹配@    NSString *topicPattern = @"#[0-9a-zA-Z\\u4e00-\\u9fa5]+#";//匹配##   

 NSString *pattern = [NSString

stringWithFormat:@"%@|%@|%@|%@",urlPattern,emotionPattern,atPattern,topicPattern];        NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern                                                                          options:NSRegularExpressionCaseInsensitive                                                                            error:&error];    NSArray *arrayOfAllMatches = [regex matchesInString:_content options:0 range:NSMakeRange(0, [_content length])];    

return arrayOfAllMatches;

}

#pragma mark 点击事件

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event

{

UITouch *touch = touches.allObjects[0];

CGPoint touchPos = [touch locationInView:self];

//    __weak typeof (HWStatusTextLabel *)weakSelf = self;

[self getTapFrameWithTouchPoint:touchPos result:^(NSString *string, NSRange range, NSInteger index) {

NSLog(@"string : %@ NSStringFromRange : %@ index : %ld",string,NSStringFromRange(range),(long)index);

}];

}

#pragma mark - getTapFrame

- (BOOL)getTapFrameWithTouchPoint:(CGPoint)point result:(void(^) (NSString *string,NSRange range , NSInteger index))resultBlock{

//判断点击的区域是否超出label的范围

CGFloat verticalOffset = 0.0f;

if (!CGRectContainsPoint(CGRectInset(self.bounds, 0, -verticalOffset),point)) return NO;

//这两句 创建了一个路径,用于表示文本在此区域内绘制着。Mac下Core Text支持在各种形状中绘制文本,比如矩形和椭圆。但iOS下仅支持矩形。在本例中,使用整个视图作为文本绘制的路径,直接将self.bounds转变为一个CGPath

CGMutablePathRef path = CGPathCreateMutable();

CGPathAddRect(path, NULL, CGRectMake(0, 0, CGRectGetWidth(self.bounds), self.bounds.size.height));

//CTFramesetter是绘制Core Text文本的最重要的一个类。用它来管理所有的字体引用和文本绘制块。现在你需要知道的仅仅是使用 CTFramesetterCreateWithAttributedString函数创建一个 CTFramesetter。在创建出CTFramesetter之后,我们可以用 CTFramesetterCreateFrame 得到文本一个渲染范围,以及绘制文本时文本显示范围,方法最终返回一个文本绘制块

//我们不能直接拿CGRect 来得到文本的范围,因为用CGRect得到的范围,和真正文本显示的范围,还是有偏差的

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self.attributedText);

CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);

//得到绘制的这块文本中有多少行字符串,放到类型为CFArrayRef里,用处和我们的数组很像

CFArrayRef lines = CTFrameGetLines(frame);

if (!lines) {//如果没有直接返回

return NO;

}

//得到行数

CFIndex count = CFArrayGetCount(lines);

//定一个origins数组,里面存放CGPoint的元素,开辟count个空间

CGPoint origins[count];

//得到每一行的坐标原点存放到origins里面

CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);

//因为在UIKit中坐标系原点在左上方,CoreText中在左下方,左下方的

CGAffineTransform transform = [self transformForCoreText];

//循环每一行

for (CFIndex i = 0; i < count; i++) {

//得到每一行的原点

CGPoint linePoint = origins[i];

//得到每一行

CTLineRef line = CFArrayGetValueAtIndex(lines, i);

//这个会涉及到字形的问题,这有个链接,可以看一下,blog.csdn.net/fengsh998/article/details/8701738

//得到每行的frame

CGRect flippedRect = [self getLineBounds:line point:linePoint];

//坐标转换后得到的frame

CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);

rect = CGRectInset(rect, 0, 0);

//在一行文本的范围内,y轴向内偏移verticalOffset距离

rect = CGRectOffset(rect, 0, -verticalOffset);

//判断点击的点是否在文本范围内,如果在,证明点击的是这一行

if (CGRectContainsPoint(rect, point)) {

//因为每行算frame高度的时候,没有加上行距,所以每行的高度,要比总体的高度除以行数,算出来的要少。所以这里要重新算点击的ponit,求出相对于点击当前行原点的point,防止点到每两行之间的空白处,也相应点击事件

CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect));

//获取在当前行的下标

CFIndex index = CTLineGetStringIndexForPosition(line, relativePoint);

//链接个数

NSInteger link_cout = self.linkArr.count;

for (int j = 0; j < link_cout; j++) {

RichLink *link = self.linkArr[j];

//每个链接的范围

NSRange link_range = link.range;

//判断下标是否在连接的范围,如果是,证明点击的该链接

if (NSLocationInRange(index, link_range)) {

if (resultBlock) {

resultBlock (link.linkStr,link.range,(NSInteger)j);

}

return YES;

}

}

}

}

return NO;

}

- (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;//这个地方我们没有把行距加上,防止点到空白处,也能触发点击事件

//这个point.y - ascent需要说一下,看我上面发的链接的博客,我们知道,她的pont.y在基线的左侧,上行高度ascent从原点到字体中最高的字形的顶部的距离

return CGRectMake(point.x, point.y - ascent, width, height);

}

//先翻转,再向下平移self.bounds.size.height,本来原点在左下方,现在在左上方

- (CGAffineTransform)transformForCoreText

{

return CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1.0f, -1.0f);

}

//对匹配出来的特殊文本进行model封装,放到linkArr数组,RichLink存放的有该文本字符串和范围

- (NSArray *)linkArr

{

NSMutableArray *linkArr = [NSMutableArray arrayWithCapacity:1];

for (NSTextCheckingResult *match in self.matchesArr)

{

NSString *substringForMatch = [self.text substringWithRange:match.range];

NSRange range = [self.text rangeOfString:substringForMatch];

RichLink *link = [RichLink new];

link.linkStr = substringForMatch;

link.range = range;

[linkArr addObject:link];

}

return linkArr;

}

@end

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容