美文网首页
CoreText 的简单使用(四)

CoreText 的简单使用(四)

作者: KG丿夏沫 | 来源:发表于2021-06-20 18:01 被阅读0次

    回顾

    前面几篇文章我们介绍了,富文本的排版,并且封装了一套自己的基于 CoreText 富文本排版库,但是实际开发中,不会只限于此,因为我们开发中页面中都是图文混排的,所以我们在之前的基础上再次修改,达到能够支持图文混排的效果。

    支持图文混排的 排版引擎

    上一篇中我们进行封装读取的时候,JSON 数据中有一个 type 值,用来确认是否是 txt 格式的,现在我们再新增一个类型 img 类型,但是在这里还需要注意的是,我们开发的时候,图片都是从服务器获取的 url,然后本地下载后使用,但是使用 CoreText 排版,我们进行绘制的时候,需要确切的宽高值,这时候图片是还没有下载下来的,那么我们就获取不到图片的宽高了,并且图片资源不需要 color 以及 size 字段,那么我们就更换下这两个字段,换成 width 和 height 字段,让服务器给我们返回图片资源的宽高,方便我们进行绘制时计算高度。下面看下数据格式:

    [{
            "width":"2074",
            "height":"1382",
            "content":"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597124636903&di=4e86100999f185237b482a27f7659ed4&imgtype=0&src=http%3A%2F%2Fyouimg1.c-ctrip.com%2Ftarget%2Ftg%2F688%2F660%2F259%2F20eedbcd17834ef3ab9730a5dab424d5.jpg",
            "type":"img"
        },
        {
            "color":"red",
            "size":"12",
            "content":"但是在实际开发的时候,不会只是简简单单显示这种,而且前后端数据交互啥的",
            "type":"txt"
        },
        {
            "width":"530",
            "height":"398",
            "content":"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597124574690&di=3f11d257b7d6b59cc26e84518cf84856&imgtype=0&src=http%3A%2F%2Fp2.so.qhimgs1.com%2Ft01dfcbc38578dac4c2.jpg",
            "type":"img"
        },
        {
            "color":"green",
            "size":"16",
            "content":"我们拿到是这种的数据的话,处理起来很麻烦,所以一般都是和后台进行约定",
            "type":"txt"
        },
        {
            "color":"blue",
            "size":"20",
            "content":"规定一种格式,方便前端进行使用,最常见的就是JSON格式直接返回排版需要的配置",
            "type":"txt"
        }
    ]
    

    然后在进行后续操作前,先了解下CTFrame内部组成,CTFrame内部是由多个CTLine组成的,每个CTLine又是有单个或多个CTRun组成的,每个CTRun代表一组显示风格一致的文本。我们不需要手动去管理CTLine以及CTRun。虽然我们不用管理CTRun,但是我们可以指定某一个CTRun的CTRunDelegate来指定绘制时的宽度、高度、排列对齐方式等。

    对于图片排版CoreText实际上是不支持的,但是我们使用CoreText绘制的时候,是在View的drawReact方法里面,所以我们可以在图片的位置预留出来空白,在drawReact方法里面绘制图片上去。

    理解完这些后,就需要开始修改我们KGCTFrameParser类,使其支持img类型的解析,并且对img类型的CTRun设置CTRunDelegate。

    改造KGCoreTextData类,增加图片相关信息,并且增加图片绘制区域相关的逻辑。

    改造KGDisPlayView类,增加绘制图片相关的逻辑。

    + (KGCoreTextData *)parseJSONContent:(NSString *)jsonContent config:(KGCTFrameParserConfig *)config{
        //创建图片信息保存数据
        NSMutableArray *imageArr = [NSMutableArray array];
        //通过JSON数据,得到富文本
        NSAttributedString *content = [self loadJSONContent:jsonContent config:config imageArray:imageArr];
        //创建CTFramesetterRef实例
        CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
        //设置绘制边界大小
        CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
        //获得实际绘制所需要的大小
        CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, restrictSize, nil);
        //因为宽度已经国定,所以在这获取实际绘制需要的高度
        CGFloat textHeight = coreTextSize.height;
        //生成CTFrameRef实例
        CTFrameRef frame = [self createFrameWithFrameSetter:frameSetter config:config height:textHeight];
        //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回实例
        KGCoreTextData *data = [[KGCoreTextData alloc] init];
        //设置实际绘制需要的实例对象
        data.ctFrame = frame;
        //设置实际绘制需要的高度
        data.height = textHeight;
        //设置图片绘制信息
        data.imageArray = imageArr;
        //因为底层库不受ARC约束,所以需要手动调用CFRelease来进行释放
        //释放生成的CTFrameRef实例
        CFRelease(frame);
        //释放CTFramesetterRef实例
        CFRelease(frameSetter);
        //返回需要的绘制数据模型
        return data;
    }
    

    然后修改loadJSONContent方法,保存当前节点图片信息,以及新建一个空白占位符。代码如下:

    #pragma mark --根据JSON数据,创建富文本
    
    
    + (CTFrameRef)createFrameWithFrameSetter:(CTFramesetterRef)frameSetter config:(KGCTFrameParserConfig *)config height:(CGFloat)height{
        //创建一个可变路径,图形上下文中要绘制的图形或线条的数学描述
        CGMutablePathRef path = CGPathCreateMutable();
        //追加矩形到可变路径
        CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
        //对核心文本框架对象的引用
        CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL);
        CFRelease(path);
        return frame;
    }
    
    + (UIColor *)colorFromDictionary:(NSDictionary *)dict{
        if ([dict[@"color"] isEqualToString:@"red"]) {
            return [UIColor redColor];
        } else if ([dict[@"color"] isEqualToString:@"blue"]) {
            return [UIColor blueColor];
        } else if ([dict[@"color"] isEqualToString:@"green"]) {
            return [UIColor greenColor];
        }else{
            return nil;
        }
    }
    
    + (NSAttributedString *)attributedingWithDictionary:(NSDictionary *)dic config:(KGCTFrameParserConfig *)config{
        //创建一个可变字典,保存默认富文本信息配置选项
        NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:[self attributesWithConfig:config]];
        //获取数据中颜色值
        UIColor *color = [self colorFromDictionary:dic];
        if (color) {
            //如果数据中给出颜色值,替换默认设置的色值
            attributes[NSForegroundColorAttributeName] = color;
        }
        CGFloat fontSize = [dic[@"size"] floatValue];
        if (fontSize > 0) {
            //获取数据返回的字体大小,如果存在,创建一个CTFontRef实例,替换默认设置项中的字体设置
            CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
            attributes[NSFontAttributeName] = (__bridge id)fontRef;
            //释放CTFontRef实例
            CFRelease(fontRef);
        }
        NSString *content = dic[@"content"];
        //根据富文本培训选项以及给定字符串创建并返回一个富文本
        return [[NSAttributedString alloc] initWithString:content attributes:attributes];
    }
    
    static CGFloat ascentCallback(void *ref){
        return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue];
    }
    
    static CGFloat descentCallback(void *ref){
        return 0;
    }
    
    static CGFloat widthCallback(void *ref){
        return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue];
    }
    
    + (NSAttributedString *)attributedingImageWithDict:(NSDictionary *)dict config:(KGCTFrameParserConfig *)config{
        //创建CTRunDelegate回调
        CTRunDelegateCallbacks callbacks;
        //初始化callbacks
        memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
        //设置回调版本号,初始化版本号为kCTRunDelegateVersion1
        callbacks.version = kCTRunDelegateVersion1;
        //为请求run委托以确定并返回在运行中字形的排版上升而调用的回调
        callbacks.getAscent = ascentCallback;
        //为请求run委托来确定并返回运行中符号的印刷下降而调用的回调
        callbacks.getDescent = descentCallback;
        //请求run委托来确定并返回运行中字形的排版宽度
        callbacks.getWidth = widthCallback;
        //创建CTRunDelegateRef实例
        CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict);
        //使用0xFFCC作为空白占位符
        unichar objectReplacementChar = 0xFFCC;
        //转换为字符串
        NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
        //获取富文本配置属性
        NSDictionary *atts = [self attributesWithConfig:config];
        //创建富文本
        NSMutableAttributedString *attributes = [[NSMutableAttributedString alloc] initWithString:content attributes:atts];
        //在指定范围内设置单个属性的值
        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attributes, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
        //释放CTRunDelegateRef实例
        CFRelease(delegate);
        return attributes;
    }
    
    + (NSAttributedString *)loadJSONContent:(NSString *)jsonContent config:(KGCTFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray{
        //JSON字符串转NSData
        NSData *data = [jsonContent dataUsingEncoding:NSUTF8StringEncoding];
        //创建一个可变富文本,保存JSON数据
        NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
        //这里做下容错处理,如果传入的json数据是非空并且有内容,进行数据转换
        if (data) {
            //因为JSON数据本身最外层就是一个数组,所以这块需要用数组去接受JSON解析后得到的值
            NSArray *arr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
            //这里再做下容错处理,如果是NSArray类,那就说明数据没有问题,可以进行下一步操作
            if ([arr isKindOfClass:[NSArray class]]) {
                //直接使用for in遍历
                for (NSDictionary *dict in arr) {
                    NSString *type = dict[@"type"];
                    //判断type类型是否是txt,因为现在只是对txt类型进行的处理,所以这块需要进行过滤
                    if ([type isEqualToString:@"txt"]) {
                        //获取到单条数据富文本信息
                        NSAttributedString *as = [self attributedingWithDictionary:dict config:config];
                        //将单条数据富文本信息拼接到总数据中
                        [result appendAttributedString:as];
                    }else if ([type isEqualToString:@"img"]) {
                        //创建图片信息保存模型
                        KGCoreTextImageData *imageData = [[KGCoreTextImageData alloc] init];
                        //设置图片连接
                        imageData.name = dict[@"content"];
                        //设置图片需要加载位置
                        imageData.position = [result length];
                        //保存到图片信息数组中
                        [imageArray addObject:imageData];
                        //获取到单条数据富文本信息
                        NSAttributedString *as = [self attributedingImageWithDict:dict config:config];
                        //将单条数据富文本信息拼接到总数据中
                        [result appendAttributedString:as];
                    }
                }
            }
        }
        return result;
    }
    
    + (KGCoreTextData *)parseJSONContent:(NSString *)jsonContent config:(KGCTFrameParserConfig *)config{
        //创建图片信息保存数据
        NSMutableArray *imageArr = [NSMutableArray array];
        //通过JSON数据,得到富文本
        NSAttributedString *content = [self loadJSONContent:jsonContent config:config imageArray:imageArr];
        //创建CTFramesetterRef实例
        CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
        //设置绘制边界大小
        CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
        //获得实际绘制所需要的大小
        CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, restrictSize, nil);
        //因为宽度已经国定,所以在这获取实际绘制需要的高度
        CGFloat textHeight = coreTextSize.height;
        //生成CTFrameRef实例
        CTFrameRef frame = [self createFrameWithFrameSetter:frameSetter config:config height:textHeight];
        //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回实例
        KGCoreTextData *data = [[KGCoreTextData alloc] init];
        //设置实际绘制需要的实例对象
        data.ctFrame = frame;
        //设置实际绘制需要的高度
        data.height = textHeight;
        //设置图片绘制信息
        data.imageArray = imageArr;
        //因为底层库不受ARC约束,所以需要手动调用CFRelease来进行释放
        //释放生成的CTFrameRef实例
        CFRelease(frame);
        //释放CTFramesetterRef实例
        CFRelease(frameSetter);
        //返回需要的绘制数据模型
        return data;
    }
    

    新建一个类用来保存图片数据,代码如下:

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface KGCoreTextImageData : NSObject
    
    /** 图片地址 */
    @property (nonatomic, copy) NSString *name;
    /** 图片位置 */
    @property (nonatomic, assign) NSInteger position;
    /** 绘制边界 */
    @property (nonatomic, assign) CGRect imagePosition;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "KGCoreTextImageData.h"
    
    @implementation KGCoreTextImageData
    
    @end
    

    修改KGCoreTextData类,新增保存图片信息数组属性。代码如下:

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface KGCoreTextData : NSObject
    
    /** CTFrameRef实例 */
    @property (nonatomic, assign) CTFrameRef ctFrame;
    /** 实际绘制所需要的高度 */
    @property (nonatomic, assign) CGFloat height;
    /** 保存图片绘制信息 */
    @property (nonatomic, copy) NSArray *imageArray;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "KGCoreTextData.h"
    #import "KGCoreTextImageData.h"
    
    @implementation KGCoreTextData
    
    - (void)setCtFrame:(CTFrameRef)ctFrame{
        if (_ctFrame != ctFrame) {
            if (_ctFrame != nil) {
                CFRelease(_ctFrame);
            }
            CFRetain(ctFrame);
            _ctFrame = ctFrame;
        }
    }
    
    - (void)dealloc
    {
        if (_ctFrame != nil) {
            CFRelease(_ctFrame);
            _ctFrame = nil;
        }
    }
    
    - (void)setImageArray:(NSArray *)imageArray{
        _imageArray = imageArray;
        [self chackImagePosition];
    }
    
    - (void)chackImagePosition{
        //容错处理,当没有图片时不再进行后续操作
        if (self.imageArray.count == 0) {
            return;
        }
        
        //获取当前CTFrame实例的行信息
        NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
        //获取行数
        int linesCount = (int)[lines count];
        //CTFrameGetLines返回的行数组中相对于路径边界盒原点的对应行的原点保存数组
        CGPoint lineOrigin[linesCount];
        //复制帧的线源范围
        CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigin);
        //设置图片位置初始值,默认为0
        int imgIndex = 0;
        //读取数组中图片信息
        KGCoreTextImageData *imageData = self.imageArray[0];
        //循环去读图片信息
        for (int i = 0; i < linesCount; i++) {
            //容错处理,如果图片信息不存在,直接返回
            if (imageData == nil) {
                break;
            }
            //读取单行信息
            CTLineRef line = (__bridge CTLineRef)lines[i];
            //获取单个CTLine中CTRun
            NSArray *runObjectArray = (NSArray *)CTLineGetGlyphRuns(line);
            for (id runOjb in runObjectArray) {
                //读取单个CTRun
                CTRunRef run = (__bridge CTRunRef)runOjb;
                //获取单个CTRun的布局属性
                NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
                //获取CTRunDelegate不透明对象的类型
                CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes 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(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
                runBounds.size.height = ascent + descent;
                //确定字符串索引的图形偏移量或偏移量
                CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                runBounds.origin.x = lineOrigin[i].x + xOffset;
                runBounds.origin.y = lineOrigin[i].y;
                runBounds.origin.y -= descent;
                
                //获取绘制路径
                CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
                //包含图形路径中所有点的边界框
                CGRect colReact = CGPathGetBoundingBox(pathRef);
                //返回一个矩形,其原点与源矩形的原点偏移
                CGRect delegateReact = CGRectOffset(runBounds, colReact.origin.x, colReact.origin.y);
                //设置图片数据区域
                imageData.imagePosition = delegateReact;
                imgIndex++;
                if (imgIndex == self.imageArray.count) {
                    imageData = nil;
                    break;
                }else{
                    imageData = self.imageArray[imgIndex];
                }
            }
        }
    }
    
    @end
    

    最后修改KGDisPlayView,代码如下:

    #import "KGDisPlayView.h"
    #import "KGCoreTextImageData.h"
    
    @implementation KGDisPlayView
    
    
    - (void)drawRect:(CGRect)rect{
        [super drawRect:rect];
        
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        CGContextTranslateCTM(context, 0, self.bounds.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        if (self.data) {
            CTFrameDraw(self.data.ctFrame, context);
        }
        if (self.data.imageArray.count > 0) {
            for (KGCoreTextImageData *obj in self.data.imageArray) {
                CGContextDrawImage(context, obj.imagePosition, KGImage(obj.name).CGImage);
            }
        }
    }
    
    @end
    

    到此框架改版完成了,然后运行下,最后效果如下:

    20200811002.png

    到这里基本上一个常见的图文混排框架就写好了,然后或许可能根据业务不同做一些微调。下一篇探索框架中对图片添加点击事件。

    系列文章:

    <a href="https://juejin.cn/post/6970879379425460255/">《CoreText的简单使用(一)》</a>

    <a href="https://juejin.cn/post/6970879593129443336/">《CoreText的简单使用(二)》</a>

    <a href="https://juejin.cn/post/6970880935327694879/">《CoreText的简单使用(三)》</a>

    <a href="https://juejin.cn/post/6970881236936081439/">《CoreText的简单使用(四)》</a>

    <a href="https://juejin.cn/post/6970881873304092686/">《CoreText 的简单使用(五)》</a>

    相关文章

      网友评论

          本文标题:CoreText 的简单使用(四)

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