解析纯文本字符串
源码github地址:https://github.com/zsmzhu/MinRichText.git
通过正则表达式解析纯文本字符串,将其分成三种类型:@xxx、网址、表情
@正则:@"@[-_a-zA-Z0-9\u4E00-\u9FA9]+"
网址正则: @"((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\.\-!@#$%^&+?:_/=<>])?)"
表情正则:@"\[[^ \[\]]+?\]"
将纯文本的NSString类型的Content转化为NSMutableAttributedString的Result返回
首先处理表情,因为表情会使用空白占位符替换,导致NSString的Range改变,在完成表情解析后才能进行下一步的@、网址解析
表情解析
使用iOS自带正则解析,得到匹配正则的字符串数组
遍历数组生成空白占位符,
注意:在此过程中将非表情字段也转换为NSAttributedString添加到需要返回的Result中
1.定义CTRunDelegateCallbacks
由此设置好表情描绘时候的大小Size,将CTRunDelegateCallbacks添加到Result的属性字典中,描绘的时候取出来使用
2.添加自定义属性字典
将表情图片的名字、Range位置、富文本类型type枚举值添加到属性字典中
链接解析、@解析
链接解析和@解析一样
遍历匹配数组并且添加自定义处理的属性字典
CoreText的绘制
重写drawRect:方法
1.由解析完成的attributedString获取描绘区域
2.由于CoreText坐标系原点为左下角,进行描绘前需要翻转坐标系(上下翻转)
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform flipVertial = CGAffineTransformMake(1, 0, 0, -1, 0, rect.size.height);
CGContextConcatCTM(context, flipVertial);
3.使用最灵活的CTRunDraw进行描绘
CoreText中三大层级关系为CTFrame、CTLine、CTRun
CTRun为相同属性的字符,CTLine由一行CTRun组成、CTFrame则是所有需要描绘的CTLine组成
首先遍历每一行CTLine
再遍历CTLine中的CTRun、获取CTRun属性
NSDictionary *attDic = (__bridge NSDictionary *)CTRunGetAttributes(run);
根据之前存储的富文本类型type枚举属性分别进行绘制
对于@和链接需要点击的字符首先绘制点击高亮背景、然后再绘制文字,不然文字会被背景覆盖
链接绘制下划线代码
// 这里需要绘制下划线,记住CTRun是不会自动绘制下滑线的
// 即使你设置了这个属性也不行
// CTRun.h中已经做出了相应的说明
// 所以这里的下滑线我们需要自己手动绘制
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
CGContextSetLineWidth(context, 0.5);
CGContextMoveToPoint(context, runBounds.origin.x, runBounds.origin.y);
CGContextAddLineToPoint(context, runBounds.origin.x + runBounds.size.width, runBounds.origin.y);
CGContextStrokePath(context);
计算绘制bounds,文字部分height由行高决定
// 获取CTLine坐标点
CGPoint originArray[lineCount];
CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), originArray);
// 获取CTLine的上行高度、下行高度
CGFloat lineAscent, lineDescent;
CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, NULL);
// 计算CTRun的bounds
CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL);
CGFloat height = lineAscent + lineDescent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
CGFloat x = originArray[i].x + xOffset;
CGFloat y = originArray[i].y - lineDescent;
CGRect runBounds = CGRectMake(x, y, width, height);
描绘表情根据之前设置的delegate重新计算bounds,
// 重新表情图片大小计算
CGFloat ascent, descent, leading, emojiHeight, emojiWidth;
emojiWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading);
emojiHeight = ascent + descent;
runBounds = CGRectMake(x, y, emojiWidth, emojiHeight);
// 获取表情图片
NSString *emojiName = attDic[kEmojiAttributeName];
UIImage *emoji = [UIImage imageNamed:emojiName];
// 绘制表情
CGContextDrawImage(context, runBounds, emoji.CGImage);
点击处理
有几个属性进行相关记录处理
@property (nonatomic, assign) UITouchPhase touchPhase;/*!< 点击状态 */
@property (nonatomic, assign) CGPoint beginPoint;/*!< 开始点击的坐标点 */
@property (nonatomic, assign) CGPoint endPoint;/*!< 结束点击的坐标点 */
@property (nonatomic, assign) CFIndex beginIndex;/*!< 开始点击的Range位置 */
@property (nonatomic, assign) CFIndex endIndex;/*!< 结束点击的Range位置 */
记录点击位置坐标点、进行翻转转换为CoreText坐标系的点。
遍历找到相对于的CTRun,获取点击事件相关信息、处理点击回调、重新绘制文字。显示高亮背景
bug待解决
@中文文字,没办法WordWrapping