美文网首页iOS UI 心得经
iOS UI 优化 - Core Text

iOS UI 优化 - Core Text

作者: JackJin | 来源:发表于2019-06-14 15:02 被阅读0次

    Core Text 富文本编辑

    Core Graphics 后面一篇文章本该对 Core Image 框架进行整理,但是基于 Core Graphics 的富文本编辑 Core Text 框架更方便讲述。而且结合自己兴趣和好玩的程度在讲述自己对 TextureASDKYYKit 详细描述。

    Core Text 结构层级

    这里小编对 Core Text 在实现的层次结构上所处位置,以及在对 UIKit 框架我们常见控件 UITextFiledUITextViewUILabel 具体实现基础。

    Core Text.png

    上图是小编根据 2018 WWDCTextKit Best PracticesIntroducing Text Kit 得出 Core GraphicsCore TextUIKit 中能够实现文本绘制的控件实现结构图。
    根据上面结构可以看出:
    (1)基于 Quartz 封装的 Core GraphicsCore Text 的实现基础。而且 Quartz 可以直接处理字体和字形将文字渲染到界面,也是在 Apple 基础库中唯一一个处理字体模块。
    (2)基于 Core Text 封装实现的 Text Kit 为在 UIKitAppKit 中文本显示控件 TypeTextFiledTypeTextView 等提供最直接的接口。

    Core Text API 族

    Core Text API 族.png

    Core Text 作为在 OS 对应的五大平台唯一拥有实现文字绘制能力的跨平 台框架,族类 API 都是表示 CTType 形式。

    Opaque Types

    集合数据类型 表示 & 使用范围
    CTFont(字体) 1、 表示字体类型参数。字体特征中基本参数表示:字体大小、字体所属的格式或者是转换矩阵等。2、 是在 UIKit 中我们经常使用设置子类类型的 UIFont(__birdge CTFont *) 表现形式,在采用 context 绘制上下文中绘制确定字体。
    CTFontCollection(字体组) 1、 表示字体组合。字体组合也即是对一组文字或者一段短文字字体集合,提供对同一段文字不同格式文字内容字体访问和获取。一句话:字体整体表示。
    CTFontDescriptor(字体描述) 1、 表示字体类型描述。字体描述可以用获取、指定或者是修改当前字体属性,字体相关属性(字体名字、位置点大小和变化等)。
    CTFrame(绘制画布) 1、 表示多行文字绘制画布。CTFrame(绘制画布)是 Core Text 实现文字绘制对外集中体现形式,其中确定绘制参数:画布区域(Range)、绘制路径(Path)和绘制文本参数信息(Attribtes)。2、 对于单行来说可以精确获取在画布:行数和每一行对应开头坐标。在获取绘制的画布时调用对应的 CTFrameDraw(CTFrame, CGContext) 在上下文中绘制文字显示详细内容。 3、 CTFrame 不仅支持在 main thread 中进行绘制,同样也支持在 background Thread 进行内容的绘制。 这也是 YYKitTexture 支持后台绘制控件实现的基础。
    CTFramesetter(画布🏭) 1、 表示字体具体绘制生成工厂。画布工厂根据要显示的富文本信息、显示画布路径和画布具体的区域(Range)来生成对应展示的显示画布的效果。2、 CTFramesetter(画布工厂)是采用 CFFrameDraw 来实现富文本编辑绘制基础,但是如果采用 CTLineDraw 或者是 CTRunDraw 形似就另当别论了。 YYText 中绘制的就是按照 CTLineDrawCTRunDraw 来绘制的。
    CTGlyphInfo(字体信息) 1、 表示在字体对于 Glyph ID 特殊的映射关系。2、
    CTLine(画布中行) 1、 表示在具体执行 Frame(画布)中一行。是组成画布绘制 Frame 中单独的一行,同时也是字体不同格式组成在一行中组成最小单元 Run(块)的集合。 2、 在实现富文本绘制的过程中可以采用 CTLineDraw(CTLine, CGContext) 方式来绘制当前行。
    CTParagaraphStyle(段落格式) 1、 段落格式表示在文本绘制时,段落段落之间设置基本参数或者单独一段信息基本格式。例如:对其样式、截取样式和排布的方向等等2、
    CTRun(块) 1、 表示在富文本绘制过程中格式相同最小单元。根据字体设置的 CTFont(字体)参数不同在绘制过程中可以分割为格式一致一个一个单元,既是 Run2、 在文本编辑过程中可以根据在画布中需要显示 Lines,然后在但单独 Line 中获得 Run 采用 CTRunDraw(CTRun, CGContext) 来实现单个 Run(块)独自绘制。
    CTRunDelegate(块协议) 1、 表示在运行时的一个运行委托,在实现计算是可以调整字形上升、下降和当前绘制字形宽高。这个 API 是我们在实现文字图片换混排的基础,在生成 Frame 时计算当前 Line 里面 Run 需要绘制素材类型,然后在改素材类型位置设置 ImageAscentDescentWidthHeight 来设置当前图片绘制参数信息,然后在实际绘制中在当前需要绘制的 Image 采用 CGContextDrawImage(CGContext, CGRect, CGImageRef) 绘制当前图片。
    CTTextTab(文本标签) 1、 表示在文本段落中样式标签,用来保存段落之间段落对其方式和位置信息。
    CTTypesetter(排版工厂) 1、 表示字体具体绘制来执行布局。可以通过 CTFramesetterCreateWithTypesetter(CTTypesetter) 生成上文展示 CTFramesetter(绘制工厂)。

    Reference

    集合数据类型 表示 & 使用范围
    Core Text Sting Attributes(富文本展示样式) 富文本下划线样式设置
    Core Text Structure(结构) CTTextTab(标签) 范围和表,查找向量 Header。
    Core Text Enumerations(枚举) Core Text 字体描述匹配、指定绑定标识符自动激活、指定 URL 字体注册失败、定义字体注册范文等等。
    Core Text Constants(常量值) Core Text 中使用富文本设置一些常量值。
    Core Text Fundations(功能) Core Text 中一些功能参数值。
    Core Text Data Types(数据分类) Core TextATS 字体参考、字体集合排序描述符回调和 CT 描述符处理进度回调。

    上面是 Core Text 所有的 API 的接口,及其在实现富文本绘制中相对相应的 API 的主要功能作用。具体 CTFramesetter 怎么样通过 NSString 来生成 CTFrameRef 然后在 UIKit 基础的显示控件上绘制出来的呢?

    Core Text 最小系统

    这里说的最小系统概念是在电子单片机中最小系统单元,这里指的是 Core Text 实现最基本文字排版。
    下面贴出在实现中经典代码:

    - (void)drawRect:(CGRect)rect {
        [super drawRect:rect];
        
        // Step 1
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        // Step 2
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        CGContextTranslateCTM(context, 0, self.bounds.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        
        // Step 3
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, self.bounds);
        
        // Step 4
        NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"Hello world! Welcome to learn RICH TEXT knowladge."];
        CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
        CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attString.length), path, NULL);
        
        // Step 5
        CTFrameDraw(frame, context);
        
        // Step 6
        CFRelease(frame);
        CFRelease(path);
        CFRelease(frameSetter);
    }
    

    继承 UIView 来自定义 FYView 然后重写 drawRect:,在上面 drawRect: 重新写上面代码。Core Text 实际绘制中主要分为:计算转换在 UIKit 的坐标、设置绘制 Path、初始化 CFFramesetter 画布工厂生成画布和在上下文中绘制。
    具体分为 6 个步骤:
    Step 1:获取当前绘制上下文 context
    Step 2:Core Text 中左下角的坐标点转换为 UIKit 左上角的坐标点;
    Step 3:设置绘制的 path 路径,把当前 UIViewbounds 设置为绘制区域;
    Step 4:通过 String 初始化 NSAttributedString 来创建 CTFramesetterRef 然后根据画布工厂创建 CTFrameRef
    Step 5:在上下文中绘制画布内容;
    Step 6:释放创建 framepathframesetterCFTypeRef 对象。

    在当前 Core Text 绘制的基础之上,我们在看一下具体绘制实现中 CTTypeRefFramesetterFrameLineRun 之间的关系,以及在实际绘制显示在界面上对应。

    Core Text 最小系统.png

    小编在实现图文混排实现中,贴出下面一段对于富文本 CFFrameRef 来计算 Image 富文本绘制布局代码。

        //获取 frameRef 中 Lines
        NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
        NSInteger lineCount = lines.count;
        //获取 frameRef 中 Each Line 开头 Point
        CGPoint lineOrigins[lineCount];
        CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
        
        NSInteger imgIndex = 0;
        //遍历 Each Line
        for (int i = 0; i < lineCount; i++) {
            CTLineRef lineRef = (__bridge CTLineRef)lines[i];
            //获取 Each Line 的 Run
            NSArray *runsArray = (NSArray *)CTLineGetGlyphRuns(lineRef);
            
            //遍历 Each Run
            for (id runObj in runsArray) {
                CTRunRef runRef = (__bridge CTRunRef)runObj;
                NSDictionary *runAttribs = (NSDictionary *)CTRunGetAttributes(runRef);
                CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttribs valueForKey:(id)kCTRunDelegateAttributeName];
                
                if (delegate == nil) {
                    continue;
                }
                
                NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
                if (![metaDic isKindOfClass:[NSDictionary class]]) {
                    continue;
                }
                
                CGRect runBounds;
                CGFloat ascent;
                CGFloat descent;
                
                //获取 run 的 width
                runBounds.size.width = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), &ascent, &descent, NULL);
                //根据上下偏移获取 run 的 height
                runBounds.size.height = ascent + descent;
                
                CGFloat xOffset = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
                runBounds.origin.x = lineOrigins[i].x + xOffset;
                runBounds.origin.y = lineOrigins[i].y;
                runBounds.origin.y -= descent;
                
                CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
                CGRect colRect = CGPathGetBoundingBox(pathRef);
                
                CGRect delegateRect = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
            }
        }
    

    从上面两段代码可以看出在 CTTypeRef 类族中类的关系如下:
    (1) CTFramesetter 通过初始化 NSAttributedString 来创建绘制画布 CTFrameRef 对应的类;
    (2) 通过画布 CTFrame 可以获取所有的 Line 基本信息,例如:行数、每行 Start Point等参数。再通过坐标转换就可以计算当前 LineUIKit 界面上布局绘制信息,在实际的绘制时可以选用 CTLineDraw(CTLineRef, CGContext) 来替代 CTFrameDraw(CTFrameRef, CGContext) 整个画布在当前上下文中绘制
    (3)效仿从 CTFrameRef 中获取对应的 Line,同样可以在 Each Line 中来获取 Core Text 最基本的单元 CTRunRef。通过获取的 Each Line 中所有的 Run,然后借助 CTRunDelegateRef 在实际绘制过程中动态设置当前 Run 块需要绘制 Rect 区域。同样在实际绘制时也可以采用 CTRunDraw(CTRunRef, CGContext) 来代替 CTLineDraw(CTLineRef, CGContext) 来单独绘制每个字形块。


    在图文混排过程中分为两种情况,根据情况的不同也可以采用不同的方式实现排版引擎实现:
    (a)对于排版的数据来源于 Server 也就是我们需要需要访问后才会获取数据,然后来初始化控件,鉴于 Network 的延迟性,我们可以预先设置通过设置 CTRunDelegate 拓展在展示时的 Image 的参数信息;
    (b)对要排版的数据信息已知,在实现的基础上对 Image 位置插入临时代替的字符串,通过 CTDelegateRef 来获取对应 Image 基本参数设置当前 Run 块的绘制时上下缩进,然后添加到富文本字符串中此时也可以记录当前插入字符串所在的位置。

    Core Text 图文混排实现

    目前以 Core Text 为基础实现图文混排实现控件 YYKit 系列组件中 YYText 实现逻辑最为清晰,瞒住的情况也最为全面。下面根据要实现图文混排的数据来源做区分来分别讲解排版引擎实现逻辑。

    来自 Server

    当需要排版的数据来自与 Server,客户端和后台来商量在模板传输数据格式。这里在本地模拟网络数据加载的数据格式采用 JSON 来实现,不过小编建议如果后台允许的情况下可以尝试 Protobuf 数据格式(后面小编会在网络协议中对该格式的数据进行详细的讲解)。

    这里对 Core Text 绘制实现步骤进行划分,然后对每一个步骤的工作进行提取来实现不同的功能。下面区分

    (1)FYEvolveDisplay:显示类,用于在富文本实际绘制,排版的图片的图片实现填充和图片及链接文本点击操作监听;
    (2)FYFrameParser:排版类,对需要排版的内容加载解析,然后生成排版;
    (3)FYFrameParseConfig:配置类,在排版绘制中文字基本参数的 model 配置参数;
    (4)FYCoreTextData:模型类,作为实际绘制中数据显示承载体;
    (5)FYCoreTextImageData:图片配置类,在排版绘制中图片基本参数的 model 配置参数;
    (6)FYCoreTextLinkData:链接文本配置类,在排版绘制中链接文字基本参数的 model 配置参数;
    (7)FYCoreTextLinkUtilts:链接文本工具类,在 FYEvolveDisplay 点击中查找判断当前点击在在具体哪个链接文字。

    下面按照(1)数据加载解析生成显示承载 --> (2)然后在绘制 --> (3)最后在点击相应步骤来贴出相关代码段

    数据解析

    //FYFrameParser
    
    //Step 1
    + (FYCoreTextData *)parseLocalImageTemplateFile:(NSString *)path config:(FYFrameParserConfig *)config {
        NSMutableArray *imageArray = [NSMutableArray array];
        NSMutableArray *linkArray = [NSMutableArray array];
        //解析数据模板
        NSAttributedString *imageTemplateAttrib  = [self loadLocalTemplateFile:path config:config imageArray:imageArray linkArray:linkArray];
        
        //根据配置信息生成显示载体
        FYCoreTextData *coreTextData = [self parseAttributeContent:imageTemplateAttrib config:config];
        coreTextData.imageArray = imageArray;
        coreTextData.linkArray = linkArray;
        
        return coreTextData;
    }
    
    //Step 2
    + (NSAttributedString *)loadLocalTemplateFile:(NSString *)path
                                           config:(FYFrameParserConfig *)config
                                       imageArray:(NSMutableArray *)imageArray
                                        linkArray:(NSMutableArray *)linkArray {
        NSData *data = [NSData dataWithContentsOfFile:path];
        NSMutableAttributedString *mutableAttrib = [[NSMutableAttributedString alloc] init];
        
        if (data) {
            NSArray *templateArray = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
            if ([templateArray isKindOfClass: [NSArray class]]) {
                for (NSDictionary *dic in templateArray) {
                    NSString *typeDic = dic[@"type"];
                    if ([typeDic isEqualToString: @"txt"]) {//parser text data
                        NSAttributedString *textAttrib = [self parseAttributedContentFormDictionary:dic config:config];
                        [mutableAttrib appendAttributedString:textAttrib];
                        
                    }else if ([typeDic isEqualToString: @"img"]) {//parser image data
                        NSAttributedString *imageAttrib = [self parseImageDataFromDictionary:dic config:config];
                        [mutableAttrib appendAttributedString:imageAttrib];
                        
                        FYCoreTextImageData *imageData = [[FYCoreTextImageData alloc] init];
                        imageData.name = dic[@"name"];
                        imageData.url = dic[@"url"];
                        imageData.position = mutableAttrib.length;
                        
                        [imageArray addObject:imageData];
                    }else if([typeDic isEqualToString:@"link"]){//parser link text data
                        NSUInteger startLoc = mutableAttrib.length;
                        
                        NSAttributedString *linkAttrib = [self parseAttributedContentFormDictionary:dic config:config];
                        [mutableAttrib appendAttributedString:linkAttrib];
                        
                        FYCoreTextLinkData *linkData = [[FYCoreTextLinkData alloc] init];
                        NSUInteger len = mutableAttrib.length - startLoc;
                        NSRange range = NSMakeRange(startLoc, len);
                        linkData.range = range;
                        linkData.title = dic[@"content"];
                        linkData.url = dic[@"url"];
                        
                        [linkArray addObject:linkData];
                    }
                }
            }
        }
        
        return mutableAttrib;
    }
    
    //Step 3
    + (NSAttributedString *)parseAttributedContentFormDictionary:(NSDictionary *)dict config:(FYFrameParserConfig *)config {
        NSMutableDictionary *attribDic = [[self attributesWithConfig:config] mutableCopy];
        
        //Color
        UIColor *color = [UIColor colorFromHexString:dict[@"color"]];
        if (color) {
            attribDic[(id)kCTForegroundColorAttributeName] = (__bridge id)color.CGColor;
        }
        
        //Size
        CGFloat fontSize = [dict[@"size"] floatValue];
        if (fontSize > 0) {
            CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
            attribDic[(id)kCTFontAttributeName] = (__bridge id)fontRef;
            CFRelease(fontRef);
        }
        
        NSString *content = dict[@"content"];
        
        return [[NSAttributedString alloc] initWithString:content attributes:attribDic];
    }
    
    //Step 4
    + (NSAttributedString *)parseImageDataFromDictionary:(NSDictionary *)dict
                                                  config:(FYFrameParserConfig *)config {
        CTRunDelegateCallbacks callbacks;
        memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
        callbacks.version = kCTRunDelegateVersion1;
        callbacks.getAscent = ascentCallback;
        callbacks.getDescent = descentCallback;
        callbacks.getWidth = widthCallback;
        
        CTRunDelegateRef delegateRef = CTRunDelegateCreate(&callbacks, (__bridge void *)(dict));
        
        unichar objectReplaceChar = 0xFFCC;
        NSString *conetnt = [NSString stringWithCharacters:&objectReplaceChar length:1];
        
        NSDictionary *attrib = [self attributesWithConfig:config];
        NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:conetnt attributes:attrib];
        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegateRef);
        
        CFRelease(delegateRef);
        
        return space;
    }
    
    //Step 5
    + (FYCoreTextData *)parseAttributeContent:(NSAttributedString *)attribContent config:(FYFrameParserConfig *)config {
        CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef) attribContent);
        
        //绘制高度
        CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
        CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, restrictSize, nil);
        CGFloat textHeight = coreTextSize.height;
        
        //生成 CTFrameRef
        CTFrameRef frameRef = [self createFrameRefWithFrameSetter:frameSetter config:config height:textHeight];
        
        FYCoreTextData *coreTextData = [[FYCoreTextData alloc] init];
        coreTextData.ctFrame = frameRef;
        coreTextData.height = textHeight;
        
        CFRelease(frameRef);
        CFRelease(frameSetter);
        
        return coreTextData;
    }
    
    //Step 6
    + (CTFrameRef)createFrameRefWithFrameSetter:(CTFramesetterRef)frameSetterref
                                         config:(FYFrameParserConfig *)config
                                         height:(CGFloat)height {
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
        
        CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetterref, CFRangeMake(0, 0), path, NULL);
        
        CFRelease(path);
        
        return frameRef;
    }
    

    上面是模拟网络请求数据模板加载本地模板生成显示载体(即画布)过程。
    Step 1:Step 2 加载本地模拟数据,然后由 Step 5 来计算绘制画布的区域和路径生成对应画布交给模型类在自定义的 UIKit 控件 drawRect: 完成绘制;
    Step 2: 获取本地的数据然后解码,遍历其中数据内容解析对应 TextImageLinkText 数据。如果是 Text 类型由 Step 3 来根据内容配置来生成对应的 NSAttributedString 数据,如果是 Image 类型就交由 Step 4 临时填充单个字符串并且通过 CTRunDelegateRef 来在运行时设置当前上下缩进,如果是 LinkText 类型的数据还是由 Step 3 来完成处理,但是记录当前 NSAttributedStringStart IndexEnd Index 位置
    Step 3:此步骤是解析 Text 类型的数据,根据设置的 FYFrameConfig 配置的基础信息对数据进行解析生成对应 NSAttributedString 类型的数据;
    Step 4:此步骤是对 Image 类型的数据类型对应的 CTRunRef 进行临时赋值一个字符,然后通过 CTRunDelegateCallbacks 来设置 Run(块)在实际绘制时 Ascent(排版上升) 和 Descent(排版下降)间距以此来作为图片绘制时的高度。然后在实际绘制过程中遍历当前 Run(块) 计算当前 Image 绘制的区域,获取图片后采用 CGContextDrawImage(CGContext, CGRect, CGImage) 绘制当前图片
    Step 5:通过 NSAttributedString 来生成 CTFrameRef 画布工厂,计算当前需要绘制内容高度由 Step 6 来生成 CTFrameRef(画布)然后赋值给 FYCoreTextData 模型类画布;
    Step 6: 利用最小单元中通过设置路径使用 CFFramesetterRef(绘制🏭)来生成对应需要排版的 CTFrameRef(画布)。

    实际绘制

    //FYEvolveDisplayView
    
    //Step 1
    - (void)drawRect:(CGRect)rect {
        [super drawRect:rect];
        
        CGContextRef contextRef = UIGraphicsGetCurrentContext();
        
        CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
        CGContextTranslateCTM(contextRef, 0, self.bounds.size.height);
        CGContextScaleCTM(contextRef, 1.0, -1.0);
        
        if (self.coreTextData) {
            CTFrameDraw(self.coreTextData.ctFrame, contextRef);
        }
        
        for (FYCoreTextImageData *imageData in self.coreTextData.imageArray) {
            UIImage *image = [UIImage imageNamed:imageData.name];
            
            if (image) {
                //CGRect rect = imageData.imageRect;
                //NSLog(@"image data rect in display view: x:%f y:%f height:%f width:%f", rect.origin.x, rect.origin.y, rect.size.height, rect.size.width);
                
                CGContextDrawImage(contextRef, imageData.imageRect, image.CGImage);
            }
        }
    }
    
    //FYCoreTextData
    
    //Step 2
    - (void)fillReplaceCharWithImagePosition {
        if (self.imageArray.count == 0) { return; }
        
        NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
        NSInteger lineCount = lines.count;
        CGPoint lineOrigins[lineCount];
        CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
        
        NSInteger imgIndex = 0;
        FYCoreTextImageData *imageData = self.imageArray[0];
        
        for (int i = 0; i < lineCount; i++) {
            if (imageData == nil) { break; }
            
            CTLineRef lineRef = (__bridge CTLineRef)lines[i];
            NSArray *runsArray = (NSArray *)CTLineGetGlyphRuns(lineRef);
            
            for (id runObj in runsArray) {
                CTRunRef runRef = (__bridge CTRunRef)runObj;
                NSDictionary *runAttribs = (NSDictionary *)CTRunGetAttributes(runRef);
                CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttribs valueForKey:(id)kCTRunDelegateAttributeName];
                
                if (delegate == nil) {
                    continue;
                }
                
                NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
                if (![metaDic isKindOfClass:[NSDictionary class]]) {
                    continue;
                }
                
                CGRect runBounds;
                CGFloat ascent;
                CGFloat descent;
                
                runBounds.size.width = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), &ascent, &descent, NULL);
                runBounds.size.height = ascent + descent;
                
                CGFloat xOffset = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
                runBounds.origin.x = lineOrigins[i].x + xOffset;
                runBounds.origin.y = lineOrigins[i].y;
                runBounds.origin.y -= descent;
                
                CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
                CGRect colRect = CGPathGetBoundingBox(pathRef);
                
                CGRect delegateRect = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
                
                imageData.imageRect = delegateRect;
                imgIndex++;
                
                if(imgIndex == self.imageArray.count) {
                    imageData = nil;
                    break;
                }else {
                    imageData = self.imageArray[imgIndex];
                }
                
            }
        }
    }
    

    Step 1:上面是在 FYEvolveDisplayViewdrawRect: 采用 CTFrameDraw(CTFrameRef, CGContext) 绘制在 UIView 上,然后从 FYCoreTextData 中逐个绘制 FYCoreTextImageData 取出在 Step 2 计算当前 Image 对一个的区域。然后使用 CGContextDrawImage(CGContext, CGRect, CGImageRef) 绘制;
    Step 2:这里在上文中有展示。主要一点:CTFrameRef 获取 Line 每行,然后在遍历每行的 Run(块)判断其 CTRunDelegateRef 对应的协议计算当前 Image 对应的区域。

    图片和链接文字点击

    //FYEvolveDisplayView
    
    //Step 1
    - (instancetype)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self deployGestureRecognizer];
        }
        return self;
    }
    
    - (void)clickComposeImageInDisplayView:(UIGestureRecognizer *)recognizer {
        CGPoint point = [recognizer locationInView:self];
        
        for (FYCoreTextImageData *imageData in self.coreTextData.imageArray) {
            CGRect imageRect = imageData.imageRect;
            CGPoint imagePosCoreText = imageRect.origin;
            
            //转换 `Core Text`(左下)坐标到 `UIKit`(左上)坐标
            imagePosCoreText.y = self.bounds.size.height - imagePosCoreText.y - imageRect.size.height;
            CGRect rectInScreen = CGRectMake(imagePosCoreText.x, imagePosCoreText.y, imageRect.size.width, imageRect.size.height);
            
            if (CGRectContainsPoint(rectInScreen, point)) {
                NSLog(@"Click image of name: %@ url: %@", imageData.name, imageData.url);
            }
        }
        
        //
        FYCoreTextLinkData *linkData = [FYCoreTextLinkUtils touchLinkTextInDisplayView:self atPoint:point data:self.coreTextData];
        
        if (linkData) {
            NSLog(@"Click Link Text. The title is: %@, The Url is %@", linkData.title, linkData.url);
        }
    }
    
    - (UIGestureRecognizer *)tapGestureRecognizer {
        if (_tapGestureRecognizer == nil) {
            _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(clickComposeImageInDisplayView:)];
            _tapGestureRecognizer.delegate = self;
        }
        
        return _tapGestureRecognizer;
    }
    
    //FYCoreTextLinkUtils
    
    //Step 2
    + (FYCoreTextLinkData *)touchLinkTextInDisplayView:(UIView *)view atPoint:(CGPoint)point data:(FYCoreTextData *)textData {
        CTFrameRef frameRef = textData.ctFrame;
        CFArrayRef lines = CTFrameGetLines(frameRef);
        if (!lines) { return nil; }
        
        CFIndex count = CFArrayGetCount(lines);
        
        CGPoint origins[count];
        CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), origins);
        
        //Translateform coordinate
        CGAffineTransform transform = CGAffineTransformMakeTranslation(0, view.bounds.size.height);
        transform = CGAffineTransformScale(transform, 1.0f, -1.0f);
        
        for (int i = 0; i < count; i++) {
            CGPoint linePoint = origins[i];
            CTLineRef line = CFArrayGetValueAtIndex(lines, i);
            
            CGRect lineRect = [self gainLineBounds:line linePoint:linePoint];
            CGRect flippedRect = CGRectApplyAffineTransform(lineRect, transform);
            
            if (CGRectContainsPoint(flippedRect, point)) {
                CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(flippedRect), point.y - CGRectGetMinY(flippedRect));
                CFIndex index = CTLineGetStringIndexForPosition(line, relativePoint);
                
                return [self linkTextAtIndex:index linkArray:textData.linkArray];
            }
        }
        
        return nil;
    }
    
    //Step 3
    + (FYCoreTextLinkData *)linkTextAtIndex:(CFIndex)index linkArray:(NSArray *)linkArray {
        FYCoreTextLinkData *linkData = nil;
        for (FYCoreTextLinkData *data in linkArray) {
            if (NSLocationInRange(index, data.range)) {
                linkData = data;
                break;
            }
        }
        
        return linkData;
    }
    
    //Step 4
    + (CGRect)gainLineBounds:(CTLineRef)line linePoint:(CGPoint)point {
        CGFloat ascent = 0.0f;
        CGFloat descent = 0.0f;
        CGFloat leading = 0.0f;
        
        CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGFloat height = ascent + descent;
        
        return CGRectMake(point.x, point.y - descent, width, height);
    }
    

    Step 1:在当前 FYEvoloveDisplayView 添加 UITapGestureRecognizer 手势识别,来识别点击的位置。遍历对应 FYCoreTextData 中图片 Rect 来查找点击位置是否对应 Image,同时执行 Step 2 来查找点击是否对应链接文字;
    Step 2:CTFrameRef 遍历每一行由 Step 4 来获取对应行 Rect 转变为 UIKit 坐标,在该行 Rect 在当前包含点击 Point 时,在由 Step 3 遍历对应输出 FYCoreTextLinkData 链接文字;
    Step 3:在点击每行 Line 中通过点击文字 Line 中的 Index 遍历链接文字对应 Range 获取对应点击 FYCoreTextLinkData
    Step 4:通过当前行 Start Point 然后获取当前 Run 对应 Rect 然后计算出对应 LineRect(区域)。

    上面代码的 Demo

    本地

    在本地获取图文混排的数据,具体内容绘制的以 YYText 实现来进行讲述。这里仅仅展示 YYText 的类图,然后贴出提出几个问题。

    YYText 类图

    YYKit(类图).png

    下面主要解决一下问题

    (1)怎么提供后台绘制的能力?
    (2)怎么计算 StringImageUIView 实现图文排序?
    (3)怎么实现 String 设置 Text 各种格式设置?
    (4)怎么实现具体绘制?
    (5)怎么实现富文本上点击事件?

    怎么提供后台绘制的能力

    //YYText
    
    + (Class)layerClass {
        // 重写 TextAsyncLayer 也即是重新定义 Layer 类
        // 实现对重定向
        return [YYTextAsyncLayer class];
    }
    
    //YYTextAsyncLayer 异步后台绘制
    
    if (task.willDisplay) task.willDisplay(self);
    _YYTextSentinel *sentinel = _sentinel;
    int32_t value = sentinel.value;
        
    BOOL (^isCancelled)() = ^BOOL() {
        return value != sentinel.value;
    };
    
    CGSize size = self.bounds.size;
    BOOL opaque = self.opaque;
    CGFloat scale = self.contentsScale;
        
    //生成背景颜色的 CGTypeRef 类型
    CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;
        
    //判断当前 size 宽高 | 如果不满住条件 | 就要返回当前 Layer Contents 桥接
    //实际绘制 获取获取当前绘制线程,在当前线程中绘制 | 提供取消机制,取消就展示 | 在设置的 CGContext 获取绘制 Image | 判断取消的基础上在 main thread 绘制
    if (size.width < 1 || size.height < 1) {
        CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
        self.contents = nil;
        if (image) {
            dispatch_async(YYTextAsyncLayerGetReleaseQueue(), ^{
                //释放 image
                CFRelease(image);
            });
        }
        if (task.didDisplay) task.didDisplay(self, YES);
        CGColorRelease(backgroundColor);
        return;
    }
        
    //获取后台配置的线程 |
    dispatch_async(YYTextAsyncLayerGetDisplayQueue(), ^{
        if (isCancelled()) {
            CGColorRelease(backgroundColor);
            return;
        }
        
        //配置 Context 上下文
        UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        if (opaque && context) {
            //将当前图形状态的 Copy 加入图形堆栈中
            CGContextSaveGState(context); {
                if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) {
                    //设置填充颜色 | 设置绘制路径 | Path 填充
                    CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
                    CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
                    CGContextFillPath(context);
                }
                if (backgroundColor) {
                    CGContextSetFillColorWithColor(context, backgroundColor);
                    CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
                    CGContextFillPath(context);
                }
            }
            //从堆栈中取出设置 | 针对指针 retain +1
            CGContextRestoreGState(context);
            CGColorRelease(backgroundColor);
        }
        task.display(context, size, isCancelled);
        if (isCancelled()) { //如果取消 | 结束 |
            UIGraphicsEndImageContext();
            
            //在主线程展示回调
            dispatch_async(dispatch_get_main_queue(), ^{
                if (task.didDisplay) task.didDisplay(self, NO);
            });
            return;
        }
        
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        
        if (isCancelled()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                if (task.didDisplay) task.didDisplay(self, NO);
            });
            return;
        }
        
        dispatch_async(dispatch_get_main_queue(), ^{
            if (isCancelled()) {
                if (task.didDisplay) task.didDisplay(self, NO);
            } else {
                
                //如果没有取消 | 把 image -> contents
                //然后返回展示
                self.contents = (__bridge id)(image.CGImage);
                if (task.didDisplay) task.didDisplay(self, YES);
            }
        });
    });
    
    //YYTextAsyncLayer 绘制
    
    [_sentinel increase];
    if (task.willDisplay) task.willDisplay(self);
        
    //通过 CGContext 设置当前绘制条件 | 调用 YYTextLayout 绘制 | CGContext 绘制绘制生成 Image | 展示
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    if (self.opaque && context) {
        CGSize size = self.bounds.size;
        size.width *= self.contentsScale;
        size.height *= self.contentsScale;
        CGContextSaveGState(context); {
            if (!self.backgroundColor || CGColorGetAlpha(self.backgroundColor) < 1) {
                CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
                CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
                CGContextFillPath(context);
            }
            if (self.backgroundColor) {
                CGContextSetFillColorWithColor(context, self.backgroundColor);
                CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
                CGContextFillPath(context);
            }
        } CGContextRestoreGState(context);
    }
        
    //调用展示 display 方法 | 显示展示的内容
    task.display(context, self.bounds.size, ^{return NO;});
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    self.contents = (__bridge id)(image.CGImage);
        
    //把绘制的内容 生成 image 然后赋值给 content
    //调用 展示函数回调 
    if (task.didDisplay) task.didDisplay(self, YES);
    }
    

    上面贴出基于 YYTextAsyncLayer 实现在 main threadbackground thread 绘制代码。可以看出两者在实际绘制的核心代码是一样的,只是在实现后台异步绘制的时候添加 cancel 机制。然后通过在 UIGraphicsGetImageFromCurrentImageContext() 获取当前 YYText 通过 YYTextLayout 绘制之后生成对应的 Imagemain thread 实现 didDisplay 的回调绘制。

    计算 StringImageUIView 实现图文排序

    这里贴出在计算 StringImageUIView(附件) 在实际计算的实现类,由于代码量较大就不一一列出给出下面两个类。

    YYTextLayout
    YYTextLine

    //YYTextLayout
    
    + (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range {
    ...
    
    //Step 9.4 
    //Line 291
    
    YYTextLine *line = [YYTextLine lineWithCTLine:ctLine position:position vertical:isVerticalForm];
    
    ...
    }
    
    
    //YYTextLine 
    
    - (void)setCTLine:(_Nonnull CTLineRef)CTLine {
        if (_CTLine != CTLine) {
            //
            if (CTLine) CFRetain(CTLine);
            if (_CTLine) CFRelease(_CTLine);
            //
            _CTLine = CTLine;
            if (_CTLine) {
                //获取当前 Line 的宽度值 || 获取当前 line 的 ascent|descent|leading
                _lineWidth = CTLineGetTypographicBounds(_CTLine, &_ascent, &_descent, &_leading);
                //获取当前 Line 的 Range
                CFRange range = CTLineGetStringRange(_CTLine);
                _range = NSMakeRange(range.location, range.length);
                if (CTLineGetGlyphCount(_CTLine) > 0) {//Line 中字形的数量, 即 CTRun
                    CFArrayRef runs = CTLineGetGlyphRuns(_CTLine);
                    CTRunRef run = CFArrayGetValueAtIndex(runs, 0);
                    //获取第一个 run 字形 | 复制到用户提供的数据缓冲区 |
                    CGPoint pos;
                    CTRunGetPositions(run, CFRangeMake(0, 1), &pos);
                    _firstGlyphPos = pos.x;
                } else {
                    _firstGlyphPos = 0;
                }
                //
                _trailingWhitespaceWidth = CTLineGetTrailingWhitespaceWidth(_CTLine);
            } else {
                _lineWidth = _ascent = _descent = _leading = _firstGlyphPos = _trailingWhitespaceWidth = 0;
                _range = NSMakeRange(0, 0);
            }
            [self reloadBounds];
        }
    }
    
    - (void)reloadBounds {
    ...
    
    //获取 当前行所有的 字形 | 当前字形的数量
        CFArrayRef runs = CTLineGetGlyphRuns(_CTLine);
        NSUInteger runCount = CFArrayGetCount(runs);
        if (runCount == 0) return;
        
        NSMutableArray *attachments = [NSMutableArray new];
        NSMutableArray *attachmentRanges = [NSMutableArray new];
        NSMutableArray *attachmentRects = [NSMutableArray new];
        for (NSUInteger r = 0; r < runCount; r++) {
            CTRunRef run = CFArrayGetValueAtIndex(runs, r);
            CFIndex glyphCount = CTRunGetGlyphCount(run);
            if (glyphCount == 0) continue;
            //根据 run 字形块获取所在 Attributes 基本设置
            NSDictionary *attrs = (id)CTRunGetAttributes(run);
            //获取不知道什么👻? Attachment 可能是 UIImage|UIView|CALayer
            //TODO
            YYTextAttachment *attachment = attrs[YYTextAttachmentAttributeName];
            if (attachment) {
                CGPoint runPosition = CGPointZero;
                CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition);
                
                ///判断当前 run 是否是 Attachment 然后获取当前 run 的上升、下移和缩进
                CGFloat ascent, descent, leading, runWidth;
                CGRect runTypoBounds;
                runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading);
                
                if (_vertical) {
                    //交换两者之间的坐标
                    YYTEXT_SWAP(runPosition.x, runPosition.y);
                    runPosition.y = _position.y + runPosition.y;
                    runTypoBounds = CGRectMake(_position.x + runPosition.x - descent, runPosition.y , ascent + descent, runWidth);
                } else {
                    // {x, y, width, height}
                    //line 中 position | run 中 runWidth 和 ascent/descent
                    runPosition.x += _position.x;
                    runPosition.y = _position.y - runPosition.y;
                    runTypoBounds = CGRectMake(runPosition.x, runPosition.y - ascent, runWidth, ascent + descent);
                }
                
                NSRange runRange = YYTextNSRangeFromCFRange(CTRunGetStringRange(run));
                //添加附件 | 当前附件 range | 当前 line 的 rect
                [attachments addObject:attachment];
                [attachmentRanges addObject:[NSValue valueWithRange:runRange]];
                [attachmentRects addObject:[NSValue valueWithCGRect:runTypoBounds]];
            }
        }
        //附件复制 
        _attachments = attachments.count ? attachments : nil;
        _attachmentRanges = attachmentRanges.count ? attachmentRanges : nil;
        _attachmentRects = attachmentRects.count ? attachmentRects : nil;
    
    ...
    }
    
    

    YYTextLayout 中调用 YYTextLine 实例方式计算当前 CTFrameRefEach Line 相关参数。
    YYTextLine 中可以看出以 Line 为单位计算当前绘制参数,然后通过 reloadBounds 来遍历当前行的 Run 获取在初始化 Attachment 找出当前 Line 中的 UIViewCALayerUIImage 来设置当前 Attachment 绘制区域和范围。
    详细信息可以参考代码注释

    实现 String 设置 Text 各种格式设置

    //YYTextAttribute 
    
    //设置当前字体显示类型
    NSString *const YYTextBackedStringAttributeName = @"YYTextBackedString";
    NSString *const YYTextBindingAttributeName = @"YYTextBinding";
    NSString *const YYTextShadowAttributeName = @"YYTextShadow";
    NSString *const YYTextInnerShadowAttributeName = @"YYTextInnerShadow";
    NSString *const YYTextUnderlineAttributeName = @"YYTextUnderline";
    NSString *const YYTextStrikethroughAttributeName = @"YYTextStrikethrough";
    //对文字进行描边
    NSString *const YYTextBorderAttributeName = @"YYTextBorder";
    NSString *const YYTextBackgroundBorderAttributeName = @"YYTextBackgroundBorder";
    NSString *const YYTextBlockBorderAttributeName = @"YYTextBlockBorder";
    //
    NSString *const YYTextAttachmentAttributeName = @"YYTextAttachment";
    NSString *const YYTextHighlightAttributeName = @"YYTextHighlight";
    NSString *const YYTextGlyphTransformAttributeName = @"YYTextGlyphTransform";
    
    //NSAttributedString+YYText
    
    - (YYTextShadow *)yy_textInnerShadowAtIndex:(NSUInteger)index {
        return [self yy_attribute:YYTextInnerShadowAttributeName atIndex:index];
    }
    
    - (id)yy_attribute:(NSString *)attributeName atIndex:(NSUInteger)index {
        if (!attributeName) return nil;
        if (index > self.length || self.length == 0) return nil;
        if (self.length > 0 && index == self.length) index--;
        return [self attribute:attributeName atIndex:index effectiveRange:NULL];
    }
    
    + (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range {
    ...
    
        if (visibleRange.length > 0) {
            layout.needDrawText = YES;
            
            //TODO TODO 
            void (^block)(NSDictionary *attrs, NSRange range, BOOL *stop) = ^(NSDictionary *attrs, NSRange range, BOOL *stop) {
                if (attrs[YYTextHighlightAttributeName]) layout.containsHighlight = YES;
                if (attrs[YYTextBlockBorderAttributeName]) layout.needDrawBlockBorder = YES;
                if (attrs[YYTextBackgroundBorderAttributeName]) layout.needDrawBackgroundBorder = YES;
                if (attrs[YYTextShadowAttributeName] || attrs[NSShadowAttributeName]) layout.needDrawShadow = YES;
                if (attrs[YYTextUnderlineAttributeName]) layout.needDrawUnderline = YES;
                if (attrs[YYTextAttachmentAttributeName]) layout.needDrawAttachment = YES;
                if (attrs[YYTextInnerShadowAttributeName]) layout.needDrawInnerShadow = YES;
                if (attrs[YYTextStrikethroughAttributeName]) layout.needDrawStrikethrough = YES;
                if (attrs[YYTextBorderAttributeName]) layout.needDrawBorder = YES;
            };
            ...
            }
        }
        
    ...
    }
    

    在初始化 String 设置字体特定的格式,然后根据提供的 Key 值类型来保存在当前 String 转化为 NSAttributedString 类型的格式中。设置当前 Line 所属的 YYTextLayout 标记当前状态,在绘制时来执行在改 LineRun 执行绘制(后面在绘制中会讲述)。

    实现具体绘制?

    //YYLabel
    
    - (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask {
    ...
    
        task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
            ...
    
            //Layout 绘制当前 UIView 界面
            [drawLayout drawInContext:nil size:size point:point view:view layer:layer debug:nil cancel:NULL];
            //跟新当前的
            ///当前 Label 的附件 此附件归属于 Layout 中 attachments 属性
            for (YYTextAttachment *a in drawLayout.attachments) {
                if ([a.content isKindOfClass:[UIView class]]) [attachmentViews addObject:a.content];
                else if ([a.content isKindOfClass:[CALayer class]]) [attachmentLayers addObject:a.content];
            }
            ...
        }
    }
    
    //YYTextLayout
    //Line 3469
    
    //TODO 绘制方法调用
    - (void)drawInContext:(CGContextRef)context
                     size:(CGSize)size
                    point:(CGPoint)point
                     view:(UIView *)view
                    layer:(CALayer *)layer
                    debug:(YYTextDebugOption *)debug
                    cancel:(BOOL (^)(void))cancel{
        @autoreleasepool {
        ...
        }
    }
    

    实现富文本上点击事件

    //YYLabel 
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
         ...
       
         if (_highlight || _textTapAction || _textLongPressAction) {
            _touchBeganPoint = point;
            _state.trackingTouch = YES;
            _state.swallowTouch = YES;
            _state.touchMoved = NO;
            //添加定时器
            [self _startLongPressTimer];
            if (_highlight) [self _showHighlightAnimated:NO];
        }
       
        ...
    }
    
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 
        ...
        
        if (_state.trackingTouch) {
            if (!_state.touchMoved) {
                ...
                if (_state.touchMoved) {
                    [self _endLongPressTimer];
                }
            }
            ...
        }
        
        ...
    }
    
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
        ...
        
        if (_state.trackingTouch) {
            [self _endLongPressTimer];
            if (!_state.touchMoved && _textTapAction) {
                ...
                
                _textTapAction(self, _innerText, range, rect);
            }
            ...
        }
        ...
    }
    

    参考资料:
    TextKit Best Practices
    Introducing Text Kit
    Advanced Text Layouts and Effects with Text Kit
    About Core Text
    About the Cocoa Text System
    About Text Handling in iOS
    The Layout Manager
    Advanced Text Processing
    Graver
    Emoji Unicode Tables
    新大陆:AsyncDisplayKit
    Optimising Autolayout
    iOS 保持界面流畅的技巧
    WebView性能、体验分析与优化
    Text Kit 学习笔记
    初识 TextKit

    相关文章

      网友评论

        本文标题:iOS UI 优化 - Core Text

        本文链接:https://www.haomeiwen.com/subject/ykcpfctx.html