CoreText实现图文混排之尺寸估算及文件选择

CoreText实现图文混排之尺寸估算及文件选择

文山会海文章:

  • CoreText实现图文混排
  • CoreText实现图文混排之点击事件
  • CoreText实现图文混排之文环绕及点击算法
  • CoreText实现图文混排之尺寸估算及文件选择

回头望,距离CoreText系列首发过去一样年吧差不多矣,看到第一篇稿子将超过1.3W的点击量一直司机为是压力进一步老,毕竟作为瞎逼逼杰出代表的始终司机有时为要是尊重一下

同一脸庄重

当时首文章的严重性目的是答复童靴们的问题,因为发童靴问过自己尺寸估算和文本选择的题目,当时由真正没有研究那么上面内容所以回复的时候都没有为闹解决方案,只提供思路。后来闲暇下来就是研究了瞬间当即简单端内容,研究后便管研究结果放出去,也算让新兴的童靴们一个思路吧。至于问我之那片独童靴,我实际想不起来你俩是哪个了,没法私信你俩了,抱歉。

废话这么多,在就进入正题,所以今天之博客中您将会晤看到如下内容:

  • CoreText做排版时怎样进行尺寸估算
  • 哪些兑现TextView中仿佛之公文选择效果
  • CoreText一些API中有早已知bug

尺寸估算

说交尺寸估算,事实上同学等该记得老驾驶员在第一篇周边中干过CoreText提供的一个尺寸估算的函数CTFramesetterSuggestFrameSizeWithConstraints
那么老驾驶员还介绍一下夫函数:

本条函数需要传入一下参数:

  • framesetter :
    需要开展尺寸估算的framesetter(即绘制工厂)对象,此目标就由要绘制的富文本即可生成。
  • stringRange :
    需要与计算尺寸的文件范围。(比如长度为200之字符串,而若只是想计算前100独字的估量尺寸来说,可以经这参数调整)。
  • frameAttributes :
    富文本的有的其它性能,这些性将会影响排版效果,这个参数稍后会细讲。
  • constraints :
    尺寸约束,就是尺寸估算的最为要命境界,其行使方式类似于[UIView sizeThatFits:size]
    中size的用法。
  • * fitRange :
    约束内之文书范围。及文件长度十分丰富,在约束尺寸内无法完整绘制时,fitRange会被赋值为束缚内而兆示的限量。

就此说经过是措施,我们得像以[UIView sizeThatFits:size]本条方式一致计算产生同截文本的预估尺寸,但是问题尚从未这么简单的到此结束:

假如想只要绘制的文件中,存在散区域来说,只能通过frameAttributes参数进行安排。

其一特性配置排除区域是其一法的:

///返回排除区域字典
NSDictionary * getExclusionDic(NSArray * paths) {
    if (paths.count == 0) {
        return NULL;
    }
    NSMutableArray *pathsArray = [[NSMutableArray alloc] init];
    [paths enumerateObjectsUsingBlock:^(UIBezierPath * obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSDictionary *clippingPathDictionary = [NSDictionary dictionaryWithObject:(__bridge id)(obj.CGPath) forKey:(__bridge NSString *)kCTFramePathClippingPathAttributeName];
        [pathsArray addObject:clippingPathDictionary];
    }];
    return [NSDictionary dictionaryWithObjectsAndKeys:pathsArray,kCTFrameClippingPathsAttributeName, nil];
}

面是一直驾驶员写的回排除区域对应的frameAttributes的函数,其中paths是一个装有想如果解除区域的路的数组(转忘了是路子需要是坐标转换后底路线)。

只是问题就是于这排除区域达到。frameAttributes可以流传一切富文本所要之习性,但是若这里传入的frameAttributes排除区域数组的确含有需要排除的区域时,计算出来的尺寸高度将会为0。而之函数如果传入的frameAttributes没有要排除的区域则计算出来的尺寸则是准确的。对于此题材发生的因由,老驾驶员并从未找到有关材料,只见面当Stack
Overflow上张别人领了这样一句:

This seems to me like an iOS7 bug. I have been tinkering around, and
under iOS6, CTFramesetterSuggestFrameSizeWithConstraints returns a
size with height larger than 0. Same code under iOS7 returns a height
of 0.

—–出自关于CTFramesetterSuggestFrameSizeWithConstraints的讨论

大约的意就是是,这是iOS7过后的bug,iOS6和之前是API倒是没什么问题。

据此我们本一旦考虑的就该是,如果确有散区域来说,我们只要什么算预估尺寸呢?

总驾驶员的想法是得到每个CTLine的尺寸后,取并集即为所有CTLine所需尺寸。然后再次对有排除区域之尺寸取并集,即为绘制区域的尺码

上述就是尽车手对有消除区域之预估尺寸的思路,以下则是代码(这个没有特别写demo,截取自老车手的DWCoreTextLabel中针对-sizeThatFits:方法的重写实现):

-(CGSize)sizeThatFits:(CGSize)size {
    ///计算绘制尺寸限制
    CGFloat limitWidth = (size.width - self.textInsets.left - self.textInsets.right) > 0 ? (size.width - self.textInsets.left - self.textInsets.right) : 0;
    CGFloat limitHeight = (size.height - self.textInsets.top - self.textInsets.bottom) > 0 ? (size.height - self.textInsets.top - self.textInsets.bottom) : 0;

    ///获取排除区域(考虑偏移矫正,保证正确绘制)
    NSArray * exclusionPaths = [self handleExclusionPathsWithOffset:self.textInsets.bottom - self.textInsets.top];
    CGRect frame = CGRectMake(self.textInsets.left, self.textInsets.bottom, limitWidth, limitHeight);
    NSDictionary * exclusionConfig = getExclusionDic(exclusionPaths, frame);
    BOOL needDrawString = self.attributedText.length || self.text.length;

    NSMutableAttributedString * mAStr = nil;
    if (needDrawString) {
        ///获取要绘制的文本(初步处理,未处理插入图片、句尾省略号、高亮)
        mAStr = getMAStr(self,limitWidth,exclusionPaths);
    }
    CTFramesetterRef frameSetter4Cal = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mAStr);
    CTFrameRef frame4Cal = CTFramesetterCreateFrame(frameSetter4Cal, CFRangeMake(0, 0), [UIBezierPath bezierPathWithRect:frame].CGPath, (__bridge_retained CFDictionaryRef)exclusionConfig);

    CFRange visibleRange = getRangeToDrawForVisibleString(frame4Cal);

    ///处理插入图片
    if (needDrawString) {
        NSMutableArray * arrInsert = self.insertImageArr.copy;
        if (arrInsert.count) {
            ///富文本插入图片占位符
            [self handleStr:mAStr withInsertImageArr:arrInsert arrLocationImgHasAdd:[NSMutableArray array]];
            ///插入图片后重新处理工厂及frame,添加插入图片后的字符串,消除插入图片影响
            CFSAFERELEASE(frameSetter4Cal)
            CFSAFERELEASE(frame4Cal)
            frameSetter4Cal = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mAStr);
            frame4Cal = CTFramesetterCreateFrame(frameSetter4Cal, CFRangeMake(0, 0), [UIBezierPath bezierPathWithRect:frame].CGPath, (__bridge_retained CFDictionaryRef)exclusionConfig);
            visibleRange = getRangeToDrawForVisibleString(frame4Cal);
        }
    }

    if (exclusionPaths.count == 0) {///如果没有排除区域则使用系统计算函数
        CGSize restrictSize = CGSizeMake(limitWidth, MAXFLOAT);
        if (self.numberOfLines == 1) {
            restrictSize = CGSizeMake(MAXFLOAT, MAXFLOAT);
        }
        CGSize suggestSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter4Cal, visibleRange, nil, restrictSize, nil);
        CFSAFERELEASE(frameSetter4Cal);
        CFSAFERELEASE(frame4Cal);
        return CGSizeMake(suggestSize.width + self.textInsets.left + self.textInsets.right, suggestSize.height + self.textInsets.top + self.textInsets.bottom);
    }

    ///计算drawFrame及drawPath
    UIBezierPath * drawP = [self handleDrawFrameAndPathWithLimitWidth:limitWidth limitHeight:limitHeight frameSetter:frameSetter4Cal rangeToDraw:visibleRange exclusionPaths:exclusionPaths];

    CFSAFERELEASE(frameSetter4Cal)
    CFSAFERELEASE(frame4Cal)

    ///绘制的工厂
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mAStr);
    ///绘制范围为可见范围加1,防止末尾省略号失效(由于path为可见尺寸,故仅绘制可见范围时有的时候末尾的省略号会失效,同时不可超过字符串本身长度)
    CFRange drawRange = CFRangeMake(0, visibleRange.length < mAStr.length ? visibleRange.length + 1 : mAStr.length);
    CTFrameRef visibleFrame = CTFramesetterCreateFrame(frameSetter, drawRange, drawP.CGPath, (__bridge_retained CFDictionaryRef)exclusionConfig);

    __block CGRect desFrame = CGRectZero;

    DWCoreTextLayout * layout = [DWCoreTextLayout layoutWithCTFrame:visibleFrame convertHeight:size.height considerGlyphs:NO];

    [layout.lines enumerateObjectsUsingBlock:^(DWCTLineWrapper * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        CGRect temp = obj.frame;
        desFrame = CGRectUnion(temp, desFrame);
    }];

    CFSAFERELEASE(frameSetter)
    CFSAFERELEASE(visibleFrame)

    desFrame = CGRectMake(0, 0, ceil(desFrame.origin.x + desFrame.size.width + self.textInsets.right), ceil(desFrame.origin.y + desFrame.size.height + self.textInsets.bottom));

    ///重新获取为矫正偏移的图片实际绘制Path
    exclusionPaths = [self handleExclusionPathsWithOffset:0];
    [exclusionPaths enumerateObjectsUsingBlock:^(UIBezierPath * obj, NSUInteger idx, BOOL * _Nonnull stop) {
        desFrame = CGRectUnion(obj.bounds, desFrame);
    }];

    CGRect limitRect = CGRectMake(0, 0, size.width, size.height);
    desFrame = CGRectIntersection(limitRect, desFrame);
    return desFrame.size;
}

主意吃可能有点措施是一味司机的工具方法,其实都非紧要,只要看注释就好了。想掌握每个方法的现实贯彻,你或需要去他的出处去探访DWCoreTextLabel。

顺带一提的凡,老司机于找这CTFramesetterSuggestFrameSizeWithConstraints函数的时光刚好对消除区域绘制时的贯彻有了新的思路。第三篇稿子中,老司机介绍的凡,在盘算出底绘制区域drawPath中直接拼接上需要排除的区域的路径,那么根据奇偶原则自然就免去了所挑选区域。而这次老驾驶员的初思路则是,在转CTFrame的套CTFramesetterCreateFrame遭到做小动作。此函数也产生frameAttributes这个参数,传入上文中提到的排除区域字典的言语也可是上消除区域的效果。

然而当下半单实现方式以效益及还有某些小区别:

drawPath拼接的笔触被,如果简单只消除区域有混合,根据奇偶原则,交集则会让当是非排除区域。而frameAttributes传入配置字典这种方式被,则交集仍为消除区域。

些微种植方案效果比

根据此特点结合镇司机的DWCoreTextLabel中之相干要求,老车手选择了更恰当的次种植思路并当DWCoreTextLabel中开了连带修改。不过这点儿栽思路并没高低的分,还是如依据现实的需求来抉择。


入选效果

这也是立有童靴问我的,当时吗是同样体面懵逼。因为是东西我以研讨TextView的下真是眷恋研究了得,不过系统就片情节连没明白,也惟有由此runtime追踪至TextView是借助UITextSelectionView这么一个私有类完成的,更多的资料啊非是众。

既然如此无能够由此网虽然有相近实现相关需求,那么我们要要好分析一下需要。事实上我们唯有需要将到每个字形的尺码,然后以上头挂一个淡蓝色的覆盖层即可模拟出选中效果。至于用到每个字形的尺码,这里我们赖以CoreText还是可以完成的。

直车手以DWCoreTextLabel中做了之类的平等重合逻辑封装:

DWCoreTextLayout

DWCoreTextLayout对象为CTFrame对象开展初始化,后活动分析出成套CTLine、CTRun及各个一个字形,并取尺寸等其有关消息。并且每个包装类包装类之间是一个看似于链表的组织,用于快速获得上一个要生一个相应的Line、Run或者Glyph。并且Line以数组形式持有者Run,而Run又盖死引用形式引用在Line,Run与Glyph之间为保障在同样的关系。有这这样的涉在,就足以很快的拿屏幕中的点转换为相应的许。

生了DWCoreTextLayout对象的存,我们会得到每个字形对应之尺寸,也就是会获得一段落文字所对应之尺寸,只要在针对应尺寸出覆盖淡蓝色选择遮罩层即可。具体实现代码也无丢,老司机于是才供思路,想看落实的讲话还是失去DWCoreTextLayout.m受到扣具体代码吧。

自身怀念蒙遮罩层各位童靴应该无以言辞下,然而这里还有一定啊就是安入选中状态。TextView中是当我们当文字上双击文字后上选择状态,那咱们捕捉双击状态的时刻或是双击手势,要么是touchBegan方法处理。我们明白我们的Label控件被也文字上加点击事件不时靠的凡touchBegan系列措施,不论是那种方式我们且要处理好相关逻辑,否则会是冲突。这里一直司机的提议是以双击手势唤起选择状态,并且用Label的点击事件有关touchEnd方法中触发。这是盖双击手势而生效取消touchesEnd的回调,这样便避免了冲突。


CoreText提供的有的函数的bug

  • CTFramesetterSuggestFrameSizeWithConstraints
    上文中提到了之函数在扩散排除区域时凡来bug的。
  • CTRunGetStringRange
    这个函数在当下文字如果在某位为免克全展示的仿添加省不怎么模式之时光,最后一个CTRun的计算range会计算错误。
  • CTLineGetOffsetForStringIndex
    这个函数同样是对省略号模式下会误的返回0。
  • 和清除区域时,我们讨论了区区种消区域之方案,但是这点儿栽方案还非能够免一个题材,那就算是当免除区域非矩形区域还跟绘图区域间距小于一个字形宽度时,CoreText绘制的字会发出有和解除区域层。这个问题我们得经过修正排除区域之职还是样来避免。暂无找到完美的解决方案。

没招


参考资料:

  • 关于CTFramesetterSuggestFrameSizeWithConstraints的讨论

本期并没有写Demo,毕竟代码量有接触老,而且彼此间依赖性大,demo几乎就是是DWCoreTextLabel全部代码。

DWCoreTextLabel

可第三首被留有Demo地址,目的就想方便童鞋们直接下载demo,不要再提问我要了,所以于面前片篇中显的岗位还发扬言demo在第三篇稿子中。但是还是产生童靴直接留言要demo的,想必一定是老司机口水话太多没耐心看下去吧。但是老司机真的以为您并原理看还无扣无异双眼的口舌使demo你啊看不理解,所以于这声明一下吧,今天朝后再也设demo的童靴,不好意思啊,我非会见回升的。

本身有特权


自我是广告

DWCoreTextLabel我早已升级至了v1.2.3版,这个版被自己补偿加了预估尺寸的盘算方案与文本选择的API。废话不说,看效果吧:

图片 1

DWCoreTextLabel

你道马上便收了?太天真了!
旋即要起点儿独广告!!!
始终车手最近为好店家测试调试,以为我们程序员追踪问题,写了一个稍物,放图:

DWLogger仓库

图片 2

DWLogger

即时是一个日志助手类,他得助您于App中一直翻输出的日志,同时不影响计算机端的日记输出。

重复多状况下客可以叫你当非连接电脑的场面下一致可翻输出的日志,这将见面救你的测试妹妹,发生问题他吧出了肯定查看问题之章程。同时他以电动备份日志至磁盘,以帮助而解析数据的早晚以,当然,他吗可以自动收集崩溃日志,当测试妹妹崩溃后,你可直接翻日志与截图而无是苦逼的夺复现。他还好辅助您也日志划分等级,以利你分等级查看日志,同时你吗得以应用搜索效果来搜寻特定日志。

喜好的口舌,给本人单Star吧~