zoukankan      html  css  js  c++  java
  • 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,这会在后面介绍。

  • 相关阅读:
    Chrome浏览器另存为时浏览器假死问题
    excel的新增日期快捷键Ctrl+;失效解决办法
    制作Visual Studio 2019 (VS 2019) 离线安装包
    Sysinternals Suite 工具包使用指南
    如何关闭Acrobat Reader DC自动更新
    MySql like模糊查询使用详解
    注册表删除我的电脑WPS云盘图标
    解除Word文档的限制编辑!
    IIS Ftp端口设置
    [UnityShader基础]12.坐标空间
  • 原文地址:https://www.cnblogs.com/summer-blog/p/6030641.html
Copyright © 2011-2022 走看看