美文网首页
富文本原理

富文本原理

作者: 简_爱SimpleLove | 来源:发表于2018-10-11 17:00 被阅读69次

    原理分析

    富文本原理图

    CTFrame作为一个整体的画布,其中有行(CTLine)组成,每行可以分为一个或多个小方块(CTRun),属性一样的字符就分在一个小方块里。 X(line的x + ctrun.width/percent ) Y(line的y) width(ctrun.width/percent) height (line.height)

    每一个CTLine的组成如下图:


    CTLine图1
    CTLine图2

    下行高度有可能为负数值,所以:

    • 行高= Ascent + |Descent| + Line Gap
    富文本绘制步骤:

    1 先需要一个字符串StringA
    2 把StringA转成attributeString,并添加相关样式
    3 生成CTFramessetter,得到CTFrame
    4 绘制CTFrameDraw

    绘制完成后,因为绘制只是显示,其他的需要额外操作。
    • 如响应相关点击事件原理:
      CTFrame包含了多个CTLine,并且可以得到每个line的起始位置与大小,计算出你响应的区域范围,然后更具你点击的坐标来判断是否在响应区。

    • 如图片显示原理:
      先用空白占位符来把位置留出来,然后再添加图片,其他还需要添加图片的点击事件的话,原理和上面一样

    另外需要注意的是:


    坐标装换

    我们需要装换坐标系,因为最开始画布的坐标系是上面左图那样的,我们需要平移旋转成上右图的坐标系。

    只是纯文字的富文本

    我们需要自定义一个label,可以封装出来。

    #import "SJTextLabel.h"
    #import <CoreText/CoreText.h>
    
    @implementation SJTextLabel {
        
        NSRange sepRange;
        CGRect sepRect;
        NSMutableArray *sepRectArr;
    }
    
    - (void)drawRect:(CGRect)rect {
        
        sepRectArr = [NSMutableArray array];
    
        NSMutableAttributedString *attriStr = [[NSMutableAttributedString alloc] initWithString:self.text attributes:nil];
        
        [attriStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:25] range:NSMakeRange(0, self.text.length)];
        
        sepRange = NSMakeRange(8, 2);
        [attriStr addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:sepRange];
        
        // 生成CTFrame 一块整体的画布
        CTFramesetterRef setterRef = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attriStr);
        
        CGPathRef pathRef = CGPathCreateWithRect(CGRectMake(0, 0, self.frame.size.width, self.frame.size.height), &CGAffineTransformIdentity);
        
        // CFRangeMake(0, 0) 传(0,0)代表的是全局范围
        CTFrameRef frameRef = CTFramesetterCreateFrame(setterRef, CFRangeMake(0, 0), pathRef, nil);
        
        CGContextRef contextRef = UIGraphicsGetCurrentContext();
        
        // 调整坐标 (需要在绘制之前调整坐标)
        CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
        CGContextTranslateCTM(contextRef, 0, self.frame.size.height);   // 先将整个坐标轴上移self的高度
        CGContextScaleCTM(contextRef, 1, -1);                           // 再X轴不动,将Y轴倒转,旋转180度
        
        // 绘制
        CTFrameDraw(frameRef, contextRef);
        
        
        // 获取信息
        
        // 从整个画布中拿到CTLine数组  CTFrame 是整个画布
        NSArray *lineAry = (__bridge NSArray *)CTFrameGetLines(frameRef);
        
        /** 下面三行代码只是另外一种获取CFLine信息的方式,可以不用写(因为坐标原因不准确,不推荐使用)*/
        // 定义一个C语言数组
        CGPoint pointAry[lineAry.count];
        // 分配内存
        memset(pointAry, 0, sizeof(pointAry));
        // 由于坐标系的关系,不直接通过这种方式拿 行(CTLine)的起始位置
        CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), pointAry);
        
        // 初始化一个CTLine 的Y值
        double heightAddUp = 0;
        // CTLine信息
        
        // 如果目标文字被截出好几个TCTRun, 用来临时存储CTRun变量的初始化
        CFRange tempRunRange = CFRangeMake(-1, -1);
        NSInteger tempRangeLength = 0;
        
        for (int i = 0; i < lineAry.count; i++) {
            
            CTLineRef lineRef = (__bridge CTLineRef)lineAry[i];
            
            CGFloat ascent = 0;
            CGFloat descent = 0;
            CGFloat lineGap = 0;
            // 计算每个CTLine的大小
            CTLineGetTypographicBounds(lineRef, &ascent, &descent, &lineGap);
            // 字的高度(即CTLine 的高度) = 上行高度 + 下行高度 + 行间距
            double runHeight = ascent + descent + lineGap;
            
            // 获取每行CTLine的CTRun数组
            NSArray *runAry = (__bridge NSArray *)CTLineGetGlyphRuns(lineRef);
            // 初始CTRun的X位置
            double startX = 0;
            // CTRun信息
            
            for (int j = 0; j < runAry.count; j++) {
                
                CTRunRef runRef = (__bridge CTRunRef)runAry[j];
                CFRange runRange = CTRunGetStringRange(runRef);
                // 后面三个是高度,这里只是求宽度,所以高度传0就好,当然也可以传上面的 ascent descent 和 lineGap
                double runWidth = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), 0, 0, 0);
                
                // 如果需要找的那段文字换行了,则被分成了两个CTRun,需要讨论处理
                
                // 1. 如果长度刚好等于目标长度,则就在同一个CTLine
                if (runRange.location == sepRange.location && runRange.length == sepRange.length) {
                    
                    NSLog(@"找到了");
                    NSLog(@"%f, %f, %f, %f", startX, heightAddUp, runWidth, runHeight);
    
                    // 计算我们需要的位置和size, rect (比如需要点击的范围)
                    sepRect = CGRectMake(startX, heightAddUp, runWidth, runHeight);
                    [sepRectArr addObject:[NSValue valueWithCGRect:sepRect]];
    
                } else {
                    // 2. 如果长度小于目标长度,则就在不同的CTLine,后面的文字被分配到了后面CTLine,属于不同的CTRun
                    
                    // 只有获取的长度还不够目标长度的时候,才走下面这个方法
                    if (runRange.length < sepRange.length && tempRangeLength < sepRange.length) {
                        
                        // 截断的第一个CTRun
                        if (runRange.location == sepRange.location && runRange.length < sepRange.length) {
        
                            NSLog(@"找到了 -- %ld -- %ld", runRange.location, runRange.length);
                            NSLog(@"%f, %f, %f, %f", startX, heightAddUp, runWidth, runHeight);
        
                            // 计算我们需要的位置和size, rect (比如需要点击的范围)
                            sepRect = CGRectMake(startX, heightAddUp, runWidth, runHeight);
                            tempRunRange = runRange;
                            tempRangeLength = runRange.length;
                            [sepRectArr addObject:[NSValue valueWithCGRect:sepRect]];
    
                        }
                        
                        if (runRange.location == tempRunRange.location + tempRunRange.length && runRange.length < sepRange.length) {
                            
                            NSLog(@"找到了 -- %ld -- %ld", runRange.location, runRange.length);
                            NSLog(@"%f, %f, %f, %f", startX, heightAddUp, runWidth, runHeight);
                            
                            // 计算我们需要的位置和size, rect (比如需要点击的范围)
                            sepRect = CGRectMake(startX, heightAddUp, runWidth, runHeight);
                            tempRunRange = runRange;
                            tempRangeLength += tempRunRange.length;
                            [sepRectArr addObject:[NSValue valueWithCGRect:sepRect]];
    
                        }
    
                    }
                    
                }
                
                // 下一个CTRun的X位置,要加上叠加上前面的CTRun的宽度
                startX += runWidth;
                
            }
            // 字的高度叠加
            // 遍历完每一行cCTLine中的CTRun过后,轮到下一行的CTLine的时候,高度要叠加上之前的CTLine的高度
            heightAddUp += runHeight;
    //        NSLog(@"%f===%f", pointAry[i].y, heightAddUp);
            
        }
        
        // 调用一次 layoutSubviews 方法
        [self setNeedsLayout];
    
    }
    
    
    - (void)layoutSubviews {
        
        [super layoutSubviews];
        
        // 对于只有单个的sepRect,可以这么做,但是如果有多个的,就要用数组存储,下面两个推荐用第二种
        
        // 1. 单个的
        if (sepRect.size.width > 0) {
            // 可以添加button和button事件,button的大小和目标大小一样,用于响应目标的点击事件
            NSLog(@"sepRect == %@", NSStringFromCGRect(sepRect));
        }
        
        // 2. 多个的sepRect,用数组来存储
        if (sepRectArr.count > 0) {
            
            NSLog(@"sepRectArr == %@", sepRectArr);
            // 然后遍历添加button和button事件
        }
        
    }
    
    // 响应点击事件的第二种方法,推荐用这种,个人感觉最好
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        UITouch *touch = [touches anyObject];
        CGPoint point = [touch locationInView:self];
        
        if (sepRectArr.count > 0) {
            
            for (NSValue *value in sepRectArr) {
                
                CGRect temRect = value.CGRectValue;
                if (CGRectContainsPoint(temRect, point)) {
                    NSLog(@"点中了");
                }
            }
    
        }
    }
    
    @end
    

    上面需要注意的是,添加点击事件,我们有两种方法。

    • 方法一:
      在layoutSubviews方法中,添加和需要点击区域同样大小的button。
    - (void)layoutSubviews {
        
        [super layoutSubviews];
        
        // 对于只有单个的sepRect,可以这么做,但是如果有多个的,就要用数组存储,下面两个推荐用第二种
        
        // 1. 单个的
        if (sepRect.size.width > 0) {
            // 可以添加button和button事件,button的大小和目标大小一样,用于响应目标的点击事件
            NSLog(@"sepRect == %@", NSStringFromCGRect(sepRect));
        }
        
        // 2. 多个的sepRect,用数组来存储
        if (sepRectArr.count > 0) {
            
            NSLog(@"sepRectArr == %@", sepRectArr);
            // 然后遍历添加button和button事件
        }
        
    }
    

    并且,如果目标文字换行了,在多个CTLine中,被分为多个不同的CTRun,这个时候,需要用数组来存储他们的rect。

    • 方法二:
      获取触摸的点的位置,判断是否在需要响应的范围内。(个人感觉这种方法比较好)
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        UITouch *touch = [touches anyObject];
        CGPoint point = [touch locationInView:self];
        
        if (sepRectArr.count > 0) {
            
            for (NSValue *value in sepRectArr) {
                
                CGRect temRect = value.CGRectValue;
                if (CGRectContainsPoint(temRect, point)) {
                    NSLog(@"点中了");
                }
            }
        }
    }
    

    应用的话,只需要在需要的地方初始化,并且将userInteractionEnabled设置为YES就好。如下:

        _sjTextLabel = [[SJTextLabel alloc] initWithFrame:CGRectMake(100, 80, 200, 500)];
        _sjTextLabel.backgroundColor = [UIColor brownColor];
        _sjTextLabel.userInteractionEnabled = YES;
        _sjTextLabel.text = @"abcddgegesghaoghoghaogahEOCEOCEOCEOCClass";
        [self.view addSubview:_sjTextLabel];
    

    文字中放图片的富文本

    需要注意的是,在drawRect方法中进行绘制的时候,我们只是用空位的NSMutableAttributedString占在图片的位置,当绘制完成过后,在找到的图片的位置添加一个UIImageView,需要的话并添加点击事件。
    图片看作一个单独的NSMutableAttributedString,所以图片前后也分别是不同的NSMutableAttributedString,最后将NSMutableAttributedString拼接起来的,并不能将图片插入到一个NSMutableAttributedString中间(我目前没有想到方法)。
    所以需要在一个字符串中添加多张图片,就要看成多个图片NSMutableAttributedString,和被图片分割的多个不同的字符串NSMutableAttributedString,然后按顺序拼接。

    #import "SJImageLabel.h"
    #import <CoreText/CoreText.h>
    #import <CoreFoundation/CoreFoundation.h>
    
    
    #define EOCCoreTextImageWidthPro @"EOCCoreTextImageWidthPro"
    #define EOCCoreTextImageHeightPro @"EOCCoreTextImageHeightPro"
    
    // 声明成静态变量,仅限于本文件夹使用,避免在别的文件夹,如果有相同的命名就报错
    static CGFloat ctRunDelegateGetWidthCallback (void * refCon) {
        
        NSDictionary *infoDict = (__bridge NSDictionary *)refCon;
        if ([infoDict isKindOfClass:[NSDictionary class]]) {
            return [infoDict[EOCCoreTextImageWidthPro] floatValue];
        }
        return 0;
    }
    
    static CGFloat ctRunDelegateGetAscentCallback (void * reCon) {
        
        NSDictionary *infoDict = (__bridge NSDictionary *)reCon;
        if ([infoDict isKindOfClass:[NSDictionary class]]) {
            return [infoDict[EOCCoreTextImageHeightPro] floatValue];
        }
        return 0;
        
    }
    
    static CGFloat ctRunDelegateGetDescentCallback (void * refCon) {
        
        return 0;
    }
    
    @implementation SJImageLabel {
        
        NSInteger ImageSpaceIndex;
        CGRect sepRect;
        UIImageView *_sJImageV;
    
    }
    
    
    - (void)drawRect:(CGRect)rect {
        
        NSMutableAttributedString *attriStr = [[NSMutableAttributedString alloc] initWithString:self.text attributes:nil];
        
        [attriStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:16] range:NSMakeRange(0, self.text.length)];
        
        // 添加图片占位符
        ImageSpaceIndex = self.text.length;
        NSMutableAttributedString *attriImageSpaceStr = [self ctRunImageSpaceWithWidth:50 height:50];
        [attriStr appendAttributedString:attriImageSpaceStr];
        
        // 添加测试数据
        NSMutableAttributedString *attriTrailStr = [[NSMutableAttributedString alloc] initWithString:@"123456789" attributes:[NSDictionary dictionaryWithObjectsAndKeys:[UIColor redColor], NSForegroundColorAttributeName, nil]];
        [attriStr appendAttributedString:attriTrailStr];
        
        // 生成CTFrame
        CTFramesetterRef frameSetterRef = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attriStr);
        CGPathRef pathRef = CGPathCreateWithRect(CGRectMake(0, 0, self.frame.size.width, self.frame.size.height), &CGAffineTransformIdentity);
        
        CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetterRef, CFRangeMake(0, 0), pathRef, nil);
        CGContextRef contextRef = UIGraphicsGetCurrentContext();
        
        // 调整坐标
        CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
        CGContextTranslateCTM(contextRef, 0, self.frame.size.height);
        CGContextScaleCTM(contextRef, 1, -1);
        
        // 绘制
        CTFrameDraw(frameRef, contextRef);
        
        // 获取信息
        NSArray *lineAry = (__bridge NSArray *)CTFrameGetLines(frameRef);
    
        CGPoint pointAry[lineAry.count];
        memset(pointAry, 0, sizeof(pointAry));
        CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), pointAry);// 由于坐标系的关系,不直接通过这种方式拿 行(CTLine)的起始位置
        
        double heightAddUp = 0; // Y
        // CTLine信息
        for (int i =0; i < lineAry.count; i++) {
            
            CTLineRef lineRef = (__bridge CTLineRef)(lineAry[i]);
            NSArray *runAry = (__bridge NSArray *)CTLineGetGlyphRuns(lineRef);
            
            CGFloat ascent = 0;
            CGFloat descent = 0;
            CGFloat lineGap = 0;
            CTLineGetTypographicBounds(lineRef, &ascent, &descent, &lineGap);
            
            double startX = 0;
            // CTRun信息
            // 字的高度
            double runHeight = ascent + descent + lineGap;
            
            for (int j = 0; j < runAry.count; j++) {
                
                CTRunRef runRef = (__bridge CTRunRef)runAry[j];
                CFRange runRange = CTRunGetStringRange(runRef);
                double runWidth = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), 0, 0, 0);
                
                if (ImageSpaceIndex == runRange.location && (ImageSpaceIndex < runRange.location + runRange.length)) {
    
                    NSLog(@"找到了");// 计算我们需要的位置和size,rect
                    NSLog(@"%f, %f, %f, %f", startX, heightAddUp, runWidth, runHeight);
                    
                    // 也可以通过runDelegateRef来获取图片高度,这样获取的图片高度是准确的(不需要减去下行高度)
    //                NSDictionary *infoDict = (__bridge NSDictionary *)CTRunGetAttributes(runRef);
    //                CTRunDelegateRef runDelegate = (__bridge CTRunDelegateRef)[infoDict objectForKey:@"CTRunDelegate"];
    //                NSDictionary *argDict = CTRunDelegateGetRefCon(runDelegate);
    //                CGFloat imageWidth = [NSString stringWithFormat:@"%@", [argDict objectForKey:@"EOCCoreTextImageWidthPro"]].floatValue;
    //                CGFloat imageHeight = [NSString stringWithFormat:@"%@", [argDict objectForKey:@"EOCCoreTextImageHeightPro"]].floatValue;
    //                sepRect = CGRectMake(startX, heightAddUp + descent, imageWidth, imageHeight);
    
                    
                    // 需要减去一个下行高度,不然,图片会比原本长度高一个下行高度的长度
                    sepRect = CGRectMake(startX, heightAddUp + descent, runWidth, runHeight - descent);
                    NSLog(@"=== %@", NSStringFromCGRect(sepRect));
     
                }
                
                startX += runWidth;
            }
            
            // 字的高度叠加
            heightAddUp += runHeight;
    
        }
        
        [self setNeedsLayout];
    }
    
    - (void)layoutSubviews {
        [super layoutSubviews];
        
        if (sepRect.size.width > 0) {
            
            if (!_sJImageV) {
                
                _sJImageV = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"1.png"]];
                [self addSubview:_sJImageV];
            }
            
            [_sJImageV setFrame:sepRect];
            // 如图图片需要点击,可以在这里添加点击事件,也可以同理纯文字富文本中,在touchesBegan方法中判断触摸点是否在sepRect范围内,从而响应事件
        }
    }
    
    
    - (NSMutableAttributedString *)ctRunImageSpaceWithWidth:(float)width height:(float)height {
        
        CTRunDelegateCallbacks callBacks;
        memset(&callBacks, 0, sizeof(CTRunDelegateCallbacks));
        
        callBacks.getWidth = ctRunDelegateGetWidthCallback;
        callBacks.getAscent = ctRunDelegateGetAscentCallback;
        callBacks.getDescent = ctRunDelegateGetDescentCallback;
        callBacks.version = kCTRunDelegateCurrentVersion;
        
        // 创建占位符
        NSMutableAttributedString *spaceAttribut = [[NSMutableAttributedString alloc] initWithString:@" "];
        
        
        // 参数动态的话,使用reCon来传递参数
        static NSMutableDictionary *argDict = nil;
        argDict = [NSMutableDictionary dictionary];
        
        [argDict setValue:@(width) forKey:EOCCoreTextImageWidthPro];
        [argDict setValue:@(height) forKey:EOCCoreTextImageHeightPro];
        
        CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callBacks, (__bridge void *)argDict);
        
        // 配置占位的属性
        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)spaceAttribut, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
        
        return spaceAttribut;
    }
    
    
    @end
    

    相关文章

      网友评论

          本文标题:富文本原理

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