原理分析
富文本原理图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
网友评论