iOS CoreText(二)

这篇主要讲利用coreText来实现图文混排和点击事件

图文混排思路

文字的绘制只需要知道文字的大小就够了,而图片的绘制不一样,需要知道图片的坐标,高度和宽度。在CoreText中,我们可以把插入的图片当做一个特殊的CTRun,通过delegate来设置图片的宽度和高度,这样就解决了图片的高度和宽度问题,但是CoreText不会自动的对图片进行绘制,因此需要我们自己找到图片的显示位置(原点坐标),然后自己进行绘制

具体实现

根据上一篇的 iOS CoreText(一)中的代码,我们需要在需要显示图片的地方,插入一个空白字符,然后设置CTRun的代理

- (void)drawTextAndImage:(CGContextRef)context size:(CGSize)size {
    NSMutableAttributedString *astring = _textString;
    //设置坐标系
    //设置字形的变换矩阵为不做图形变换
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    //平移方法,将画布向上平移一个屏幕高度
    CGContextTranslateCTM(context, 0, size.height);
    //缩放方法,x轴缩放系数为1,则不变,y轴缩放系数为-1,则相当于以x轴为轴旋转180度
    CGContextScaleCTM(context, 1, -1);
    //这次的重点
    //设置CTRun代理
    CTRunDelegateCallbacks callBacks;
    memset(&callBacks, 0, sizeof(CTRunDelegateCallbacks));
    
    callBacks.version = kCTRunDelegateVersion1;
    callBacks.getAscent = ascentCallbacks;
    callBacks.getDescent = descentCallbacks;
    callBacks.getWidth = widthCallbacks;
    
    CTRunDelegateRef delegate = CTRunDelegateCreate(&callBacks, (void *)astring);
    
    //创建空白字符
    unichar placeHolder = 0xFFFC;
    NSString *placeHolderString = [NSString stringWithCharacters:&placeHolder length:1];
    NSMutableAttributedString *placeHolderAttributedString = [[NSMutableAttributedString alloc]initWithString:placeHolderString];
 
    NSDictionary *attributedDic = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)delegate, kCTRunDelegateAttributeName,nil];
    [placeHolderAttributedString setAttributes:attributedDic range:NSMakeRange(0, 1)];
    CFRelease(delegate);
    
    //将图片插入
    [astring insertAttributedString:placeHolderAttributedString atIndex:astring.length/2];
   
    //创建path
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
  
    //绘文字
    CTFramesetterRef frameRef = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)astring);
    CTFrameRef fref = CTFramesetterCreateFrame(frameRef, CFRangeMake(0, astring.length), path, NULL);
    CTFrameDraw(fref, context);
    
    //绘图
    UIImage *image = [UIImage imageNamed:@"tj_Image"];
    CGRect imageRect = [self calculateImageRect:fref];
    CGContextDrawImage(context, imageRect, image.CGImage);
    
    CFRelease(path);
    CFRelease(fref);
    CFRelease(frameRef);
}
#pragma mark ---CTRUN代理---
CGFloat ascentCallbacks (void *ref) {
    return 11;
}

CGFloat descentCallbacks (void *ref) {
    return 7;
}

CGFloat widthCallbacks (void *ref) {
    return 36;
}

和上次一样的我们就不讲了,从设置代理开始说起
先要设置代理的回调CTRunDelegateCallbacks callBacks;,包括了四个需要设置的属性,版本号、上边距、下边距和宽度,后面对应的是C的函数名,负责确定图片的宽度和高度。
设置完成后,穿件一个空白字符unichar placeHolder = 0xFFFC
将空白字符转换成NSString,再转换成NSMutableAttributedString
创建一个字典,key为kCTRunDelegateAttributeName,value为我们创建的delegate(__bridge为OC对象和CF对象之间的桥接),本质上addAttributes:就是add一个字典,这点要理解
然后将空白字符插入要显示的位置

 //绘图
//我们要显示的图片
    UIImage *image = [UIImage imageNamed:@"tj_Image"];
//自己定义的方法,为了得到图片的坐标和大小
    CGRect imageRect = [self calculateImageRect:fref];
//将图片绘制到指定的地方
    CGContextDrawImage(context, imageRect, image.CGImage);

我们现在主要来讲讲calculateImageRect这个方法

先来说说这个方法的思路:
我们先获取到所有的CTLine,然后遍历每个CTLine中的CTRun的,取出CTRun对应的Attributes字典,判断字典中是否有key为kCTRunDelegateAttributeName的value,如果有,就是我们插入的图片的位置。

- (CGRect)calculateImageRect:(CTFrameRef)frame {
    //先找CTLine的原点,再找CTRun的原点
    NSArray *allLine = (NSArray *)CTFrameGetLines(frame);
    NSInteger lineCount = [allLine count];
    //获取CTLine原点坐标
    CGPoint points[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
    CGRect imageRect = CGRectMake(0, 0, 0, 0);
    for (int i = 0; i < lineCount; i++) {
        CTLineRef line = (__bridge CTLineRef)allLine[i];
        //获取所有的CTRun
        CFArrayRef allRun = CTLineGetGlyphRuns(line);
        CFIndex runCount = CFArrayGetCount(allRun);
        
        //获取line原点
        CGPoint lineOrigin = points[i];
        
        
        for (int j = 0; j < runCount; j++) {
            CTRunRef run = CFArrayGetValueAtIndex(allRun, j);
            NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
            if (delegate == nil) {
                //暂时不用关注这部分代码,主要用于点击事件
                NSString *textClickString = [attributes valueForKey:@"textClick"];
                if (textClickString != nil) {
                    [textFrameArray addObject:[NSValue valueWithCGRect:[self getLocWith:frame line:line run:run origin:lineOrigin]]];
                }
                
                continue;
            }
           //获取图片的Rect
            imageRect = [self getLocWith:frame line:line run:run origin:lineOrigin];
        }
    }
    return imageRect;
}

对照上面的思路来看,代码不复杂

- (CGRect)getLocWith:(CTFrameRef)frame line:(CTLineRef)line run:(CTRunRef)run origin:(CGPoint)point {
    CGRect boundRect;
    CGFloat ascent = 0.0f;
    CGFloat descent = 0.0f;
    CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
    boundRect.size.width = width;
    boundRect.size.height = ascent + descent;
    
    //获取x偏移量
    CGFloat xoffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
    boundRect.origin.x = point.x + xoffset;
    boundRect.origin.y = point.y - descent;
    
    //获取BoundingBox
    CGPathRef path = CTFrameGetPath(frame);
    CGRect colRect = CGPathGetBoundingBox(path);

    return CGRectOffset(boundRect, colRect.origin.x, colRect.origin.y);
}
    CGFloat ascent = 0.0f;
    CGFloat descent = 0.0f;
    CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
    boundRect.size.width = width;
    boundRect.size.height = ascent + descent;

这里获取的widthascentdescent,其实就是我们在CTRunDelegateCallbacks callBacks中设置的那几个函数

CGFloat xoffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);

这句代码就是获取该CTRun对于CTLine的x轴偏移,知道偏移量和CTLine的原点,我们就可以计算出图片的原点。

boundRect.origin.y = point.y - descent;

主要是为了让图片能居中显示(可以自己调节试试)
这样图片的Rect就获取到了

现在来说说点击事件

当我们为某段文字或者某张图片设置点击事件,主要利用了本质上addAttributes:就是add一个字典这一特性,这样我们就可以直接在字典中定义一个特殊的key,用来判断该CTRun是否是具有点击事件的CTRun

LJTextView *textView = [[LJTextView alloc] initWithFrame:CGRectMake(0, 64, width, height - 64)];
textView.backgroundColor = [UIColor whiteColor];
textView.textString = [[NSMutableAttributedString alloc]initWithString:@"123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123"];
    
NSDictionary *dic = @{@"textClick":@"click",NSBackgroundColorAttributeName:[UIColor redColor]};
[textView.textString addAttributes:dic range:NSMakeRange(24, 4)];

上面的代码我们就定义了已一个textClick的特殊key

NSString *textClickString = [attributes valueForKey:@"textClick"];
if (textClickString != nil) {
      [textFrameArray addObject:[NSValue valueWithCGRect:[self getLocWith:frame line:line run:run origin:lineOrigin]]];
}

获取图片Rect的时候,我们同时获取了拥有点击事件的CTRun的Rect并把他记录在textFrameArray数组中,方面后面进行判断

- (CGRect)convertRectToWindow:(CGRect)rect {
    return CGRectMake(rect.origin.x, self.bounds.size.height - rect.origin.y - rect.size.height, rect.size.width, rect.size.height);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    [textFrameArray enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger idx, BOOL * _Nonnull stop) {
        CGRect rect = [value CGRectValue];
        CGRect convertRect = [self convertRectToWindow:rect];
        if (CGRectContainsPoint(convertRect, point)) {
            NSString *message = [NSString stringWithFormat:@"点击了%lu",(unsigned long)idx];
            UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"提示" message:message delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil];
            [alert show];
        }
    }];
}

convertRectToWindow主要用于转换坐标系,上面代码主要用于判断点击的位置在不在坐标范围内,在则响应,不在则不处理

其实CoreText并不复制,只要了解,后面就好在工作中运用了
下一篇我们介绍YYLable的实现机制

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

推荐阅读更多精彩内容