iOS阅读器实践系列(一)coretext纯文本排版基础

前言:之前做了公司阅读类的App,最近有时间来写一下阅读部分的实现过程,供梳理逻辑,计划会写一个系列希望能涉及到尽量多的方面与细节,欢迎大家交流、吐槽、拍砖,共同进步。

阅读的排版用的是coretext,这篇介绍用coretext实现基本的排版功能。

关于coretext的实现原理,可以查看文档或其他资料,这里就不介绍了,只介绍如何应用coretext来实现一个简单的文本排版功能。

因为coretext是离屏排版的,即在将内容渲染到屏幕之前,内容的排版工作的已经完成了。

排版过程大致过程分为 步:

一、由原始文本数据和需要的相关配置来得到属性字符串。

二、由属性字符串得到CTFramesetter

三、由CTFramesetter和绘制区域得到CTFrame

四、最后将CTFrame渲染到视图的上下文中

1、由原始文本数据和需要的相关配置来得到属性字符串

这一部最关键的是得到相关配置,这些配置可能包括文本对齐方式、段收尾缩进、行高等,下面是一些相关配置属性:

@interface CTFrameParserConfigure : NSObject

@property (nonatomic, assign) CGFloat frameWidth;
@property (nonatomic, assign) CGFloat frameHeight;

//字体属性
@property (nonatomic, assign) CGFloat wordSpace;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) NSString *fontName;
@property (nonatomic, assign) CGFloat fontSize;

//段落属性
@property (nonatomic, assign) CGFloat lineSpace;
@property (nonatomic, assign) CTTextAlignment textAlignment; //文本对齐模式
@property (nonatomic, assign) CGFloat firstlineHeadIndent; //段首行缩进
@property (nonatomic, assign) CGFloat headIndent;  //段左侧整体缩进
@property (nonatomic, assign) CGFloat tailIndent;  //段尾缩进
@property (nonatomic, assign) CTLineBreakMode lineBreakMode;  //换行模式
@property (nonatomic, assign) CGFloat lineHeightMutiple; //行高倍数器(它的值表示原行高的倍数)
@property (nonatomic, assign) CGFloat maxLineHeight; //最大行高限制(0表示无限制,是非负的,行高不能超过此值)
@property (nonatomic, assign) CGFloat minLineHeight;  //最小行高限制
@property (nonatomic,assign) CGFloat paragraphBeforeSpace;  //段前间距(相对上一段加上的间距)
@property (nonatomic, assign) CGFloat paragraphAfterSpace; //段尾间距(相对下一段加上的间距)

@property (nonatomic, assign) CTWritingDirection writeDirection;  //书写方向

@property (nonatomic, assign) CGFloat lineSpacingAdjustment;  //The space in points added between lines within the paragraph (commonly known as leading).

@end

接下来我们要利用这些属性,生成我们需要的配置,在我们根据我们的需要给这些属性赋值以后,利用下面的方法来得到我们需要的配置:

//返回文本所有属性的集合(以字典形式),包括字体、段落等
- (NSDictionary *)attributesWithConfig:(CTFrameParserConfigure *)config
{
    //段落属性
    CGFloat lineSpacing = config.lineSpace;
    CGFloat firstLineIndent = config.firstlineHeadIndent;
    CGFloat lineIndent = config.headIndent;
    CGFloat tailIndent = config.tailIndent;
    CTLineBreakMode lineBreakMode = config.lineBreakMode;
    CGFloat lineHeightMutiple = config.lineHeightMutiple;
    CGFloat paragraphBeforeSpace = config.paragraphBeforeSpace;
    CGFloat paragraphAfterSpace = config.paragraphAfterSpace;
    CTWritingDirection writeDirect = config.writeDirection;
    CTTextAlignment textAlignment = config.textAlignment;
    const CFIndex kNumberOfSettings = 13;
    CTParagraphStyleSetting paragraphSettings[kNumberOfSettings] = {
        { kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing },
        { kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &lineSpacing },
        { kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &lineSpacing },
        { kCTParagraphStyleSpecifierAlignment, sizeof(textAlignment), &textAlignment },
        { kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(firstLineIndent), &firstLineIndent },
        { kCTParagraphStyleSpecifierHeadIndent, sizeof(lineIndent), &lineIndent },
        { kCTParagraphStyleSpecifierTailIndent, sizeof(tailIndent), &tailIndent },
        { kCTParagraphStyleSpecifierLineBreakMode, sizeof(lineBreakMode), &lineBreakMode },
        { kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(lineHeightMutiple), &lineHeightMutiple },
        { kCTParagraphStyleSpecifierLineSpacing, sizeof(lineHeightMutiple), &lineHeightMutiple },
        { kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(paragraphBeforeSpace), &paragraphBeforeSpace },
        { kCTParagraphStyleSpecifierParagraphSpacing, sizeof(paragraphAfterSpace), &paragraphAfterSpace },
        { kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(writeDirect), &writeDirect },
    };

    CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(paragraphSettings, kNumberOfSettings);

    /**
     *   字体属性
     */
    CGFloat fontSize = config.fontSize;
    //use the postName after iOS10
//    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)config.fontName, fontSize, NULL);
    CTFontRef fontRef = CTFontCreateWithName(NULL, fontSize, NULL);
//    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"TimesNewRomanPSMT", fontSize, NULL);

    UIColor * textColor = config.textColor;

    //设置字体间距
    long number = config.wordSpace;
    CFNumberRef num = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt8Type, &number);

    NSMutableDictionary * dict = [NSMutableDictionary dictionary];
    dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
    dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
    dict[(id)kCTKernAttributeName] = (__bridge id)num;
    dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef;

    CFRelease(num);
    CFRelease(theParagraphRef);
    CFRelease(fontRef);
    return dict;
}

上述过程为先根据上面提供的段落属性值生成段落属性,然后生成字体、字体间距及字体颜色等属性,然后依次将他们存入字典中。

需要注意的地方是 CTParagraphStyleSetting 为C语言的数组,需在创建时指定数组元素个数。

创建的CoreFoundation库中的对象需要手动释放(大部分到create方法生成的对象)

另外在系统升级到iOS10以后,在调节字体大小重新排版时,变得很慢,用Instrument查了一下,发现

CTFontRef fontRef = CTFontCreateWithName((CFStringRef)config.fontName, fontSize, NULL);

这句代码执行时间很长,查找资料发现是字体造成的,iOS10需要用相应的POST NAME。

2、由属性字符串得到CTFramesetter

// 创建 CTFramesetterRef 实例
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mabStr);

3、由CTFramesetter和绘制区域得到CTFrame

这一步的关键是要得到绘制的区域:

// 获得要绘制的区域的高度
    CGSize restrictSize = CGSizeMake(viewWidth, CGFLOAT_MAX);
    CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), nil, restrictSize, nil);
    CGFloat textHeight = coreTextSize.height;

然后生成CTFrame:

//生成绘制的区域
+ (CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter frameWidth:(CGFloat)frameWidth stringRange:(CFRange)stringRange orginY:(CGFloat)originY height:(CGFloat)frameHeight
{
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0, originY, frameWidth, frameHeight)); //此时path的位置值都是coretext坐标系下的值

    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, stringRange, path, NULL);

    CFRelease(frame);
    CFRelease(path);

    return frame;
}

这里需要注意的地方就是代码中注释的地方,在排版过程中使用的坐标都是在coretext坐标系下的,即原点在屏幕左下角。

4、将CTFrame渲染到视图的上下文中

这一步是要在视图类的drawRect方法中将上步得到的CTFrame绘制出来:

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];

    //将坐标系转换为coretext下的坐标系
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

    if (ctFrame != nil)
    {
        CTFrameDraw(ctFrame, context);
    }
}

这一步的关键是坐标系的转换,因为ctFrame中包含的绘制区域是在coretext坐标系下,所以在绘制时应先将坐标系转换为coretext坐标系再绘制,才能保证绘制位置正确。

如果渲染时需要精确到行或字体可用CTLine与CTRun,这会在后面介绍。

时间: 2024-12-22 05:05:56

iOS阅读器实践系列(一)coretext纯文本排版基础的相关文章

iOS阅读器实践系列(三)图文混排

本篇介绍coretext中的图文混排,这里暂用静态的内容,即在文本中某一固定位置插入图片,而不是插入位置是根据文本内容动态插入的(要实现这一效果需要写一个文本解析器,将原信息内容解析为某些特定格式的结构来标示出特定的类型(比如文字.图片.链接等),然后按照其结构中的属性配置,生成属性字符串,之后渲染到视图中). 这部分的思路参考唐巧大神的blog. 在第一篇介绍过coretext是离屏渲染的,即在将内容渲染到屏幕上之前,coretext已完成排版工作.coretext排版的第一步是组织数据,即由

iOS阅读器实践系列(四)阅读视图方案

阅读过程中文章内容可能需要很多屏才能展示完,那么现在的问题是创建多少个视图来渲染内容. 经过考虑,决定用两个视图,利用手势来控制不断来回切换来实现. 首先定义两个私有的实体的view对象: @property (nonatomic, strong) DisplyManagerView *buffer1; @property (nonatomic, strong) DisplyManagerView *buffer2; buffer1与buffer2就是用于渲染内容的两个视图. 其次还要定义一个用

iOS React Native实践系列一

Facebook 在 React.js Conf 2015 大会上推出了基于 JavaScript 的开源框架 React Native React Native 结合了 Web 应用和 Native 应用的优势,可以使用 JavaScript 来开发 iOS 和 Android 原生应用.在 JavaScript 中用 React 抽象操作系统原生的 UI 组件,代替 DOM 元素来渲染等. React Native 使你能够使用基于 JavaScript 和 React 一致的开发体验在本地

(android高仿系列)今日头条 --新闻阅读器 (三) 完结 、总结 篇

从写第一篇今日头条高仿系列开始,到现在已经过去了1个多月了,其实大体都做好了,就是迟迟没有放出来,因为我觉得,做这个东西也是有个过程的,我想把这个模仿中一步一步学习的过程,按照自己的思路写下来,在根据碰到的知识点和问题,并且罗列出这些东西的知识点和使用方法.如果你单纯的把做好的一个DEMO拿去改改用用,那样,你永远不知道里面用到的内容是涉及到什么知识点,用什么方法实现,那样就没有多少提升价值而言了. 近期都是在通过开发文档把以前的一些东西重新过一遍,看好多网友都催促想要新版本的,那我就在这里先把

SwiftHN阅读器应用IOS源码

SwiftHN是用Swift语言编写的Hacker News阅读器,同时采用了iOS 8最新的API. <ignore_js_op> <ignore_js_op> 详细说明:http://ios.662p.com/thread-1969-1-1.html

三天独立开发的iOS端CSDN博客阅读器上线了

作为CSDN博客的忠实读者,作为一个iOS开发者,实在是不能忍受手机上无法看CSDN博客的不便,五一节抽空做了一个简单的CSDN博客阅读器,第一版仅提供博客首页热门文章查看功能和阅读个人博客功能.如果你认为有其他值得实现的优秀功能或者发现了bug,欢迎及时联系我. 由于第一天上架,AppStore还无法搜索到.从5.17日起,直接搜索csdn博客即可. 下载地址:https://itunes.apple.com/us/app/csdn-bo-ke/id991337359?l=zh&ls=1&

iOS - 小说阅读器分章节,支持正则分章节和按字数分章节

最近做了一个WIFI传书本地阅读功能,有所收获在这里记录下吧. 用户下载的书籍分为两种,一种是有章节格式的,比如 第一章,001章.等,这种可以用正则来直接分章节,还有绝大多数书籍是没有这种格式的,这种如果整本书来直接解析的话,对CPU要求比较大,可能会卡死闪退,所有手动分章节还是很有必要的,这种情况下我们采用按照两千字来分. 话不多说,开始吧. 1.WIFI传书把书传到APP沙盒里,这里我们采用的是 GCDWebServer ,很方便,这里就不做陈述了. 2.将沙盒里面的 .txt 文件转成

webApp 阅读器项目实践

这是一个webApp 阅读器的项目,是慕课网的老师讲授的一个实战,先给出项目源码在GitHub的地址:https://github.com/yulifromchina/MobileWebReader. 项目属于麻雀虽小,但五脏俱全的类型,对于前端新手来说,还是很有学习价值. 一.项目成果展示 二.项目所用技术 语言:Html,css,js 插件: zepto.js: 使用于移动端的js库,语法与jquery相似,但增加了触摸等移动端事件,去掉了对浏览器兼容的代码,因此更轻量级 jquery.ba

iOS依赖注入框架系列(一):介绍Typhoon

介绍 作为这一系列的一部分,我不会去考虑依赖性倒置原则,或模式依赖注入的理论 -理所当然地认为读者已经做好充分准备,以确保了解禅宗,并直接进入实践(链接探索中给出的理论帖子的结尾). 台风框架 -是最有名的和流行的执行DI容器Objective-C的应用和斯威夫特. 该项目是相当年轻的-第一次提交被做在2012年底,但它已得到了很多球迷 . 特别值得一提它的创始人的积极支持该项目(其中一个,顺便说一句,生活和在鄂木斯克作品) -满足最成熟的问题十分钟,几个小时来讨论整个团队加入. 为什么我们需要