CoreText富文本

一、坐标系

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.富文本 图文混排

实现思路:

  1. CTFrameDraw本身是不能直接绘制图片的,可在str中通过空字符占位,并设置CTRunDelegateCallbacks,这样在创建frameref的时候,会使用delegate设置的asecnt、decent为图片留好位置。
  2. 遍历run,判断run是否有delegate,若有,获取大小信息,CGContextDrawImage绘制图片
  3. 代码实现:
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翻转
}

效果如图:
这里写图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值