一、坐标系
iOS主要有有2种坐标系,UIKity下坐标系(原点左上),Core Graphics/QuartZ 2Dy上坐标系(原点左下)。我们这里用画线和画图来解释两种不同坐标系
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextMoveToPoint(context, 0, 2);
CGContextAddLineToPoint(context, self.bounds.size.width/2, self.bounds.size.height/2);
CGContextStrokePath(context);
你会看到一条”\”斜线,说明context是y下坐标系
CGContextRef context = UIGraphicsGetCurrentContext();
UIImage *img = [UIImage imageNamed:@"zj"];
CGContextDrawImage(context, CGRectMake(0, 0, img.size.width, img.size.height), [UIImage imageNamed:@"zj"].CGImage);//y上坐标系
你会看在左上角有一张颠倒的图片,说明CGContextDrawImage的绘制是y上坐标系
通常我们用CTM做翻转才可正确显示
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0, -1.0);
可是为什么同样在drawRect:里画线跟绘图坐标系不一样呢?
坐标系是基于context的,context是一个画布,栈式管理,坐标系通过CTM可以转换。
iOS目前有五种context
Bitmap Graphics Context
PDF Graphics Context
Window Graphics Context
Layer Graphics Context
Printer Graphics Context
关键点
1. In iOS, a drawing context returned by an UIView.
2. In iOS, a drawing context created by calling the
UIGraphicsBeginImageContextWithOptions function.
3. UIGraphicsGetCurrentContext默认返回时y下坐标系
4. CGContextDrawImage是y上坐标系
5. UIImage的DrawRect是经过处理的y下坐标系
6. UIGraphicsBeginImageContextWithOptions是y上坐标系
总结:
其实UIGraphicsGetCurrentContext获取到默认Layer Graphics Context是y下坐标的,所以画线没问题。画图用到CGContextDrawImage,其实CGContextDrawImage使用的是Bitmap Graphics Context这个context是y上坐标系,需要进行坐标转换。
二、文本绘制
1.使用NSAttributedString,创建framesetter,最后通过CTFrameDraw绘制出文本
UIFont *font = [UIFont systemFontOfSize:16];
UIColor *color = [UIColor blueColor];
NSString *text = @"排版系统中文本显示的一个重要的过程就是字符到字形的转换,字符是信息本身的元素,而字形是字符的图形表征,字符还会有其它表征比如发音。 字符在计算机中其实就是一个编码,某个字符集中的编码,比如Unicode字符集,就囊括了大都数存在的字符。 而字形则是图形";
NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName:font,NSForegroundColorAttributeName:color}];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)str);
CGSize size = CTFramesetterSuggestFrameSizeWithConstraints(
framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(contentWidthMax, NSUIntegerMax), NULL);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, contentWidthMax, size.height));
CTFrameRef frameref = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CGContextRef context = UIGraphicsGetCurrentContext();
CTFrameDraw(frameref, context);
CFRelease(frameref);
CFRelease(path);
CFRelease(framesetter);
你会发现,CTFrameDraw绘制的文本也是颠倒的,需要经过CTM翻转
2.行距、字距调整、对齐方式
//设置字体间距
long kerning = 5;
CFNumberRef kerningNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt8Type, &kerning);
[str addAttribute:(id)kCTKernAttributeName value:(__bridge id)kerningNum range:NSMakeRange(0,[str length])];
CFRelease(kerningNum);
CTTextAlignment alignment = kCTTextAlignmentJustified;//对齐方
//创建CTParagraphStyleSetting样式数组
CTParagraphStyleSetting theSettings[4] = {
{kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment),&alignment}, //对齐方式
{kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &linespacing},//行间距
{kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &linespacing},
{kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &linespacing}
};
CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, 4);
[str addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge id)theParagraphRef range:NSMakeRange(0, str.length)];
CFRelease(theParagraphRef);
三.富文本 图文混排
1.字形Glyph
首先我们先了解下字形Glyph的几个概念
基础原点(Origin)
首先是位于基线上
处于基线最左侧的位置
行间距(Leading)
行与行 之间的间距
上行高度(Ascent)和下行高度(Decent)
上行高度(Ascent) >>> 字形的最高点 ~ 基线的距离 >>>正数
下行高度(Decent) >>> 字形的最低点 ~ 基线的距离 >>>正数
lineHeight:行高 >>> 整个红色框的高度
Ascent:上行高度 >>> 红色框顶部线 ~ 绿色基线 的距离
Decent:下行高度 >>> 绿色基线 ~ 黄色框顶部线 的距离
leading:行间距 >>> 整个黄色框的高度
lineHeight(行高) = Ascent(上行高度) + Decent(下行高度) + Leading(行间距)
2.文字排版的层级关系
CFAttributedStringRef :属性字符串,用于存储需要绘制的文字字符和字符属性
CTFramesetterRef:通过CFAttributedStringRef进行初始化,作为CTFrame对象的生产工厂,负责根据path创建对应的CTFrame
CTFrame:用于绘制文字的类,可以通过CTFrameDraw函数,直接将文字绘制到context上
CTLine:在CTFrame内部是由多个CTLine来组成的,每个CTLine代表一行
CTRun:每个CTLine又是由多个CTRun组成的,每个CTRun代表一组显示风格一致的文本(CTRun可以混入图片,UI控件信息等等,CTRun可以设置自己想要的尺寸)
3.富文本 图文混排
实现思路:
- CTFrameDraw本身是不能直接绘制图片的,可在str中通过空字符占位,并设置CTRunDelegateCallbacks,这样在创建frameref的时候,会使用delegate设置的asecnt、decent为图片留好位置。
- 遍历run,判断run是否有delegate,若有,获取大小信息,CGContextDrawImage绘制图片
- 代码实现:
CGFloat fontSize = 16;
UIFont *font = [UIFont systemFontOfSize:fontSize];
CGFloat contentWidthMax = [UIScreen mainScreen].bounds.size.width;
NSString *imgTag = @"<image>";
NSString *text = @"排版系统中文本显示的一个重要的过程就是字符到字形的转换,字符是信息本身的元素,而字形是字符的图形表征,字符还会有其它表征比如发音。 字符在计算机中其实就是一个编码,某个字符集中的编码,比如Unicode字符集,就囊括了大都数存在的字符。 而字形则是图形,<image>一般都存储在字体文件中,字形也有它的编码,也就是它在字体中的索引。 一个字符可以对应多个字形(不同的字体,或者同种字体的不同样式:粗体斜体等);多个字符也可能对应一个字形,比如字符的连写( Ligatures)。 ";
NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithString:text attributes:[self attributesWithConfig]];
NSRange rang = [text rangeOfString:imgTag];
CTRunDelegateCallbacks imageCallbacks;
imageCallbacks.version = kCTRunDelegateVersion1;
imageCallbacks.dealloc = RunDelegateDeallocCallback;
imageCallbacks.getAscent = RunDelegateGetAscentCallback;
imageCallbacks.getDescent = RunDelegateGetDescentCallback;
imageCallbacks.getWidth = RunDelegateGetWidthCallback;
//创建CTRun回调
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&imageCallbacks, (__bridge void *)@{@"height":@50,@"width":@60});//@{@"height":@19,@"width":@22}
// //这里为了简化解析文字,所以直接认为最后一个字符是需要显示图片的位置,对需要显示图片的位置,都用空字符来替换原来的字符,空格用于给图片留位置
unichar placeHolder = 0xFFFC;//创建空白字符
NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];//已空白字符生成字符串
NSMutableAttributedString *imageAttributedString = [[NSMutableAttributedString alloc] initWithString:placeHolderStr attributes:[self attributesWithConfig]];
// //设置图片预留字符使用CTRun回调
[imageAttributedString addAttribute:(NSString *)kCTRunDelegateAttributeName
value:(__bridge id)runDelegate
range:NSMakeRange(0, 1)];
[str replaceCharactersInRange:rang withAttributedString:imageAttributedString];
CFRelease(runDelegate);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)str);
CGSize size = CTFramesetterSuggestFrameSizeWithConstraints(
framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(contentWidthMax, NSUIntegerMax), NULL);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, contentWidthMax, size.height));
CTFrameRef frameref = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CGContextRef context = UIGraphicsGetCurrentContext();//Layer Graphics Context y下坐标系
CGContextSaveGState(context);
//翻转坐标系
CGContextSetTextMatrix(context,CGAffineTransformIdentity); //设置字形变换矩阵为CGAffineTransformIdentity,也就是说每一个字形都不做图形变换
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CTFrameDraw(frameref, context);
CFArrayRef array = CTFrameGetLines(frameref);
NSUInteger linecount = CFArrayGetCount(array);
CGPoint lineOrigins[linecount];
CTFrameGetLineOrigins(frameref, CFRangeMake(0, linecount), lineOrigins);
for (NSInteger idx = linecount - 1; idx >= 0 ; idx--) { //因为最后一行是y:0开始的
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(array, idx);
CGPoint p = lineOrigins[idx]; //取行原点
/*
ascent : 从原点到字体中最高(这里的高深都是以基线为参照线的)的字形的顶部的距离,ascent是一个正值
descent : 下行高度n从原点到字体中最深的字形底部的距离
leading:行距line gap
*/
CGFloat descent = 0, ascend, leading;
CTLineGetTypographicBounds(line, &ascend, &descent, &leading);
NSLog(@"font---ascend:%f,descent:%f,leading:%f lineHeight:%f",font.ascender,font.descender,font.leading,font.lineHeight);
// 坐标原点是左上角,但文本绘制是首行是从底部开始绘制的(因为文本是颠倒的)
NSLog(@"ascend:%f,descent:%f,leading:%f total:%f p:%@",ascend,descent,leading,ascend+descent+leading,NSStringFromCGPoint(p));
//绘制图片
CFArrayRef runs = CTLineGetGlyphRuns(line);
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CGFloat runAscent;
CGFloat runDescent;
//获取每个CTRun
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
CTRunDelegateRef delegate =
(__bridge CTRunDelegateRef)([runAttributes valueForKey:(id)kCTRunDelegateAttributeName]);
// 如果delegate是空,表明不是图片
if (!delegate)
continue;
NSUInteger glyphcount = CTRunGetGlyphCount(run);
for (NSUInteger u = 0; u < glyphcount; ++u) {
CGRect glyphRect;
//调整CTRun的rect
glyphRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(u, 1), &runAscent, &runDescent, NULL);
NSLog(@"width = %f", glyphRect.size.width);
glyphRect =
CGRectMake(p.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location + u, NULL),
p.y - runDescent,
glyphRect.size.width,
runAscent + runDescent);
CGContextDrawImage(context, glyphRect, [UIImage imageNamed:@"zj"].CGImage);
}
}
}
CGContextRestoreGState(context);
CFRelease(frameref);
CFRelease(path);
CFRelease(framesetter);
其中 attributesWithConfig 方法返回attributes配置:
- (NSDictionary *)attributesWithConfig {
CGFloat fontSize = 16;
UIFont *font = [UIFont systemFontOfSize:fontSize];
CGFloat lineSpacing = 20;
NSNumber *horSpacing = @(0.1); //字体间距
UIColor *textColor = [UIColor blueColor];
const CFIndex kNumberOfSettings = 5;
CTParagraphStyleSetting lineBreakMode;
CTLineBreakMode lineBreak = kCTLineBreakByCharWrapping;
lineBreakMode.spec = kCTParagraphStyleSpecifierLineBreakMode;
lineBreakMode.value = &lineBreak;
lineBreakMode.valueSize = sizeof(CTLineBreakMode);
CTTextAlignment alignment = kCTTextAlignmentJustified;//对齐
//创建CTParagraphStyleSetting样式数组
CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
{kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment),&alignment}, //对齐方式
{kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing},
{kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &lineSpacing},
{kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &lineSpacing},
lineBreakMode};
CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[NSForegroundColorAttributeName] = textColor;
dict[NSFontAttributeName] = font;
dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef;
dict[(id)kCTKernAttributeName] = horSpacing;
CFRelease(theParagraphRef);
return dict;
}
这里需要说明一下,遍历line的时候我用的是从尾行遍历,因为line的原点是从y:0开始的,即使通过CTM翻转,尾行在视觉上是在底部,但仍然不会改变其line基线原点。如下日志:
2018-04-20 18:49:55.931342+0800 CoreTextTest[31568:14168089] ascend:15.234375,descent:3.859375,leading:0.480000 total:19.573750 p:{0, 4}
2018-04-20 18:49:55.932383+0800 CoreTextTest[31568:14168089] ascend:13.760000,descent:2.240000,leading:0.480000 total:16.480000 p:{0, 21}
2018-04-20 18:49:55.933272+0800 CoreTextTest[31568:14168089] ascend:15.234375,descent:3.859375,leading:0.480000 total:19.573750 p:{0, 39}
2018-04-20 18:49:55.934026+0800 CoreTextTest[31568:14168089] ascend:15.234375,descent:3.859375,leading:0.480000 total:19.573750 p:{0, 58}
2018-04-20 18:49:55.934662+0800 CoreTextTest[31568:14168089] ascend:50.000000,descent:5.440000,leading:0.480000 total:55.920000 p:{0, 76}
2018-04-20 18:49:55.945277+0800 CoreTextTest[31568:14168089] ascend:15.234375,descent:3.859375,leading:0.480000 total:19.573750 p:{0, 130}
2018-04-20 18:49:55.945847+0800 CoreTextTest[31568:14168089] ascend:15.234375,descent:3.859375,leading:0.480000 total:19.573750 p:{0, 149}
2018-04-20 18:49:55.946274+0800 CoreTextTest[31568:14168089] ascend:15.234375,descent:3.859375,leading:0.480000 total:19.573750 p:{0, 168}
2018-04-20 18:49:55.983225+0800 CoreTextTest[31568:14168089] ascend:13.760000,descent:2.240000,leading:0.480000 total:16.480000 p:{0, 185}
2018-04-20 18:49:55.983871+0800 CoreTextTest[31568:14168089] ascend:13.760000,descent:2.240000,leading:0.480000 total:16.480000 p:{0, 201}
p:{0, 4} 是最后一行『Ligatures』的原点位置,我们可以暂且理解为坐标系是y上的
最新效果:
注意:
1.context 是y下坐标系,但CTFrameDraw、CGContextDrawImage的绘制是倒序绘制(最后一行是从y:0开始),字体垂直颠倒
经过CTM翻转后,可认为 CTFrameDraw 坐标是y上的,因为翻转并不会改变line原点坐标(最后一行依然是从y:0开始)
CTFrameGetLines获取的line是从首行开头的
2.需要注意的是,font的ascender\descenter和Glyph的不是一回事,
Glyph是最终要显示在UI上的尺寸(有delegate的情况下会不一样)
font是固定是字体固有属性,descenter为负值
3.原点是绘制line的baseline,以首个Glyph计算得出原点为准,所以,在创建attributedStr空白占位的时候,需要同时设置与正文一致的attributed!
PS:
可能需要的相关引入
#import <CoreText/CTFramesetter.h>
#import <CoreText/CTRunDelegate.h>
#import <CoreText/CTStringAttributes.h>
四、其他 :行数限制
在很多业务场景下,经常会对长文本进行行数限制显示
思路:
1.判断是否超过行数
2.取限制数的最后一行line,该行run的数量runsCount,以及最后一个run
3.判断runsCount > 1 ,则取该run范围进行替换;否则,直接替换『更多』文案
CFArrayRef lines = CTFrameGetLines(frame);
NSUInteger lineCount = CFArrayGetCount(lines);
//处理行数限制
if (limit > 0 && lineCount > limit) {
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, limit - 1);
CFArrayRef runs = CTLineGetGlyphRuns(line);//获取runs
NSUInteger runsCount = CFArrayGetCount(runs);
CTRunRef run = CFArrayGetValueAtIndex(runs, runsCount-1);//获取本行最后的run
CFRange range = CTRunGetStringRange(run);//获取run所处的字符范围
NSString *more = /*你的更多文案*/;
if (runsCount > 1) {
[str replaceCharactersInRange:NSMakeRange(range.location, str.length - range.location) withAttributedString:/*你的更多文案*/];
}else{
NSUInteger location = range.location + range.length - more.length;
[str replaceCharactersInRange:NSMakeRange(location, str.length - location) withAttributedString:/*你的更多文案*/];
}
/*
用str重新创建ctframe
*/
}
五:链接响应
在富文本中,通常有超链接文本,可以响应用户的点击操作
思路:
data:
1.一般需要有特定的文本类型用以区分不同的文本区域
2.在构造attributedString的时候,将type存入attributed(同上加不同颜色区分link)
NSString *linkTag_text = @"点我响应操作";
NSString *text = [NSString stringWithFormat:@"集,就囊括了大都数存在的字符。 而字形则是图形,%@一般都存储在字体文件中,字形也有它的编码,也就是它在字体中的索引。 一个字符可以对应多个字形(不同的字体,或者同种字体的不同样式:粗体斜体等);多个字符也可能对应一个字形,比如字符的连写( Ligatures)。 ",linkTag_text];
NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithString:text attributes:[self attributesWithConfig]];
[str addAttribute:@"atrType" value:@"txt" range:NSMakeRange(0, str.length)];
NSRange rang = [str.string rangeOfString:linkTag_text];
[str addAttribute:@"atrType" value:@"link" range:rang];
[str addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:rang];
_atrStr = str;
CGFloat contentWidthMax = [UIScreen mainScreen].bounds.size.width;
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.atrStr);
CGSize size = CTFramesetterSuggestFrameSizeWithConstraints(
framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(contentWidthMax, NSUIntegerMax), NULL);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, contentWidthMax, size.height));
_frameref = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
view:
1.使用attrStr创建_frameref,CTFrameDraw绘制
CGContextRef context = UIGraphicsGetCurrentContext();//Layer Graphics Context y下坐标系
//画at及详情 等的点击
// 选中高亮
UIColor *highlightColor = [UIColor greenColor];
//翻转坐标系
CGContextSetTextMatrix(context,CGAffineTransformIdentity); //设置字形变换矩阵为CGAffineTransformIdentity,也就是说每一个字形都不做图形变换
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CTFrameDraw(_frameref, context);
2.重写touchesBegan、touchesEnded等触摸事件,在触摸事件中判断当前point是否落在link上,
获得当前attrName(我们这里的type)的范围effectiveRange,
并计算到其range(link在str中的范围),若手势end时依然在相同范围内则触发链接处理。
判断核心方法:
先判断touches点击的点location是落在哪一个line上,
//获取每一行
CFArrayRef lines = CTFrameGetLines(_frameref);
NSUInteger linecount = CFArrayGetCount(lines);
CGPoint origins[linecount];
//获取每行的原点坐标
CTFrameGetLineOrigins(_frameref, CFRangeMake(0, linecount), origins);
CTLineRef line = NULL;
CGPoint lineOrigin = CGPointZero;
for (NSUInteger i = 0; i < linecount; i++) {
CGPoint origin = origins[i];
// NSLog(@"[%ld]=%@",i,NSStringFromCGPoint(origin));
//坐标转换,把每行的原点坐标转换为uiview的坐标体系
CGFloat y = self.bounds.size.height - origin.y;
//判断点击的位置处于那一行范围内
if ((location.y <= y) && (location.x >= origin.x)) {
line = CFArrayGetValueAtIndex(lines, i);
lineOrigin = origin;
break;
}
}
if (!line) {
return;
}
注意,判断时这里需要考虑坐标转换的问题,location是y下坐标,而origin是y上坐标的,那么,这里先简单的将view高度减去原点y坐标(0是在view底部),只有location的y比其小,那么就是在改line的范围里
根据得到的line,和location获取是点赞str的哪个字体
//获取点击位置所处的字符位置,就是相当于点击了第几个字符,最终判断是否落在link上
CFIndex index = CTLineGetStringIndexForPosition(line, location);
if (index < self.atrStr.length) {
NSRange effectRange;
[self.atrStr attribute:@"atrType"
atIndex:index
longestEffectiveRange:&effectRange
inRange:NSMakeRange(0, self.atrStr.length)];
NSDictionary *attrs = [self.atrStr attributesAtIndex:index longestEffectiveRange:nil inRange:effectRange];
NSString *value = (NSString*)[attrs objectForKey:@"atrType"];
if ([value isEqualToString:@"link"]) {//判断是否是Link
//effectRange为当前点击link的range
}
}
effectRange便是link的范围range,
在touchesEnded完成的时候再按改方法判断一次effectRange是否相同,若则不处理操作
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//获取UITouch对象
UITouch *touch = [touches anyObject];
//获取触摸点击当前view的坐标位置
CGPoint location = [touch locationInView:self];
//。代码省略。。判断是否落在link上,并得到link的范围selRange。。。
self.selectedRange = selRange;
}
手势end时判断是否依然在link上
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//获取UITouch对象
UITouch *touch = [touches anyObject];
//获取触摸点击当前view的坐标位置
CGPoint location = [touch locationInView:self];
NSRange selRange;
//。代码省略。。判断是否落在link上,并得到link的范围selRange。。。
BOOL isEqualRange = NSEqualRanges(self.selectedRange, selRange);
if (!isEqualRange) {
self.selectedRange = NSMakeRange(NSNotFound, 0);
}
if (self.selectedRange.length) {
__weak __typeof(self) wself = self;
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"点了链接了");
wself.selectedRange = NSMakeRange(NSNotFound, 0);
});
}
}
}
3.在计算range的过程中,换算该range位于当前view的rect,并触发重绘,为该区域CGContextSetFillColorWithColor加颜色,形成高亮区域。
核心方法:
获取每行line的range,判断与link的range是否有交集,有则把该line上的link的rect换算出来,存入rectarr数组待用
//range为当前点击link的range
CFArrayRef lines = CTFrameGetLines(_frameref);
CGPoint origins[CFArrayGetCount(lines)];
//获取每行的原点坐标
CTFrameGetLineOrigins(_frameref, CFRangeMake(0, 0), origins);
NSMutableArray *rectarr = [NSMutableArray new];
for (NSUInteger u = 0; u < CFArrayGetCount(lines); ++u) {
CGPoint origin = origins[u];
CTLineRef line = CFArrayGetValueAtIndex(lines, u);
CFRange curRange = CTLineGetStringRange(line);//获得line的range
NSRange linerange = NSMakeRange(curRange.location, curRange.length);
NSRange intersectRange = NSIntersectionRange(linerange, range);//得交集
if (intersectRange.length != 0) {
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, intersectRange.location, NULL);
CGFloat descent = 0, ascend, leading;
CTLineGetTypographicBounds(line, &ascend, &descent, &leading);
CGRect rect;
rect.origin.x = xOffset;
rect.size.height = ascend + descent;
rect.origin.y = origin.y - descent; //用于绘制高亮,因为当前经过了CTM旋转,绘制用的是y上坐标
CGFloat nOffset =
CTLineGetOffsetForStringIndex(line, intersectRange.location + intersectRange.length, NULL);
rect.size.width = nOffset - xOffset;
[rectarr addObject:[NSValue valueWithCGRect:rect]];
if (intersectRange.location + intersectRange.length - 1 >= range.location + range.length - 1) {
break;
}
}
}
self.touchedRects = [rectarr copy];//存入属性备用
[self setNeedsDisplay];
CTLineGetStringRange :获取line在str中的range
NSIntersectionRange :得交集,判断是否line是否有link部分
CTLineGetOffsetForStringIndex : 获得str中index在绘制后的x坐标
这里需要注意的是,计算的rect是需要用来在content中绘制图形,而绘制的图形使用的是y上坐标,与origin原点使用的是相同的y上坐标,因此不需要考虑坐标系转换的问题。
绘制高亮区域
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
//....
//Y下坐标绘制(经CTM旋转后,为Y上坐标)
[self.touchedRects enumerateObjectsUsingBlock:^(NSValue *obj, NSUInteger idx, BOOL *_Nonnull stop) {
CGRect frame = [obj CGRectValue];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:3];
CGContextSetFillColorWithColor(context, highlightColor.CGColor);
[path fill];
}];
//.....CTM翻转
}
效果如图: