富文本原理

2018-10-11  本文已影响69人  简_爱SimpleLove

原理分析

富文本原理图

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

每一个CTLine的组成如下图:


CTLine图1
CTLine图2

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

富文本绘制步骤:

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

绘制完成后,因为绘制只是显示,其他的需要额外操作。

另外需要注意的是:


坐标装换

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

只是纯文字的富文本

我们需要自定义一个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

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

- (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
上一篇 下一篇

猜你喜欢

热点阅读