本文的内容主要见到的是如何使用CoreText设置高亮的内容的特殊效果,比如带有特殊颜色和下划线的链接。以及这些高亮内容的点击效果和点击事件处理
其它文章:
CoreText入门(一)-文本绘制
CoreText入门(二)-绘制图片
CoreText进阶(三)-事件处理
CoreText进阶(四)-文字行数限制和显示更多
CoreText进阶(五)- 文字排版样式和效果
CoreText进阶(六)-内容大小计算和自动布局
CoreText进阶(七)-添加自定义View和对其
效果
Demo:CoreTextDemo
单行内容点击效果
图片点击效果
多行内容点击效果
点击事件处理
点击事件的处理基本思路就是使用CTFrame对象获取到所有的CTRun对象,遍历CTRun对象,判断CTRun位置的元素是否可以点击,需要以及几个步骤
- 给NSMutableAttributedString设置特殊内容属性,表示这个NSMutableAttributedString对应的CTRun(可能是多个)是可以点击的
- 从CTFrame获取到CTRun,遍历CTRun,取出在上一步设置的特殊内容,计算CTRun最终渲染显示的位置,记录保存到对应的可点击元素上
给NSMutableAttributedString设置特殊内容属性的代码:
// 链接设置特殊内容
- (NSAttributedString *)linkAttributeStringWithLinkItem:(YTLinkItem *)linkItem {
NSMutableAttributedString *linkAttributeString = [[NSMutableAttributedString alloc] initWithString:linkItem.link attributes:[self linkTextAttributes]];
NSDictionary *extraData = @{YTExtraDataAttributeTypeKey: @(YTDataTypeLink),
YTExtraDataAttributeDataKey: linkItem,
};
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)linkAttributeString, CFRangeMake(0, linkItem.link.length), (CFStringRef)YTExtraDataAttributeName, (__bridge CFTypeRef)(extraData));
return linkAttributeString;
}
// 图片设置特殊内容以及CTRunDelegate
- (NSAttributedString *)imageAttributeStringWithImageItem:(YTImageItem *)imageItem size:(CGSize)size {
// 创建CTRunDelegateCallbacks
CTRunDelegateCallbacks callback;
memset(&callback, 0, sizeof(CTRunDelegateCallbacks));
callback.getAscent = getAscent;
callback.getDescent = getDescent;
callback.getWidth = getWidth;
// 创建CTRunDelegateRef
NSDictionary *metaData = @{@"width": @(size.width), @"height": @(size.height)};
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData));
// 设置占位使用的图片属性字符串
// 参考:https://en.wikipedia.org/wiki/Specials_(Unicode_block) U+FFFC OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document.
unichar objectReplacementChar = 0xFFFC;
NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1] attributes:[self defaultTextAttributes]];
// 设置RunDelegate代理
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
// 设置附加数据,设置点击效果
NSDictionary *extraData = @{YTExtraDataAttributeTypeKey: @(YTDataTypeImage),
YTExtraDataAttributeDataKey: imageItem,
};
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), (CFStringRef)YTExtraDataAttributeName, (__bridge CFTypeRef)(extraData));
CFRelease(runDelegate);
return imagePlaceHolderAttributeString;
}
计算特殊内容CTRun的位置并且把保存的代码
- (void)calculateContentPositionWithBounds:(CGRect)bounds {
int imageIndex = 0;
if (imageIndex >= self.images.count) {
return;
}
// CTFrameGetLines获取但CTFrame内容的行数
NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
// CTFrameGetLineOrigins获取每一行的起始点,保存在lineOrigins数组中
CGPoint lineOrigins[lines.count];
CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0; i < lines.count; i++) {
CTLineRef line = (__bridge CTLineRef)lines[i];
NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
for (int j = 0; j < runs.count; j++) {
CTRunRef run = (__bridge CTRunRef)(runs[j]);
NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
if (!attributes) {
continue;
}
// 获取附加的数据
NSDictionary *extraData = (NSDictionary *)[attributes valueForKey:YTExtraDataAttributeName];
if (extraData) {
NSInteger type = [[extraData valueForKey:YTExtraDataAttributeTypeKey] integerValue];
YTBaseDataItem *data = (YTBaseDataItem *)[extraData valueForKey:YTExtraDataAttributeDataKey];
NSLog(@"run = (%@-%@) type = %@ data = %@", @(i), @(j), @(type), data);
// CTLineGetOffsetForStringIndex获取CTRun的起始位置
CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
CGFloat yOffset = lineOrigins[i].y;
// 找到代理则开始计算图片位置信息
CGFloat ascent;
CGFloat desent;
// 可以直接从metaData获取到图片的宽度和高度信息
CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
CGFloat height = ascent + desent;
if ([data isKindOfClass:YTBaseDataItem.class]) {
// 由于CoreText和UIKit坐标系不同所以要做个对应转换
CGRect ctClickableFrame = CGRectMake(xOffset, yOffset, width, height);
// 将CoreText坐标转换为UIKit坐标
CGRect uiKitClickableFrame = CGRectMake(xOffset, bounds.size.height - yOffset - ascent, width, height);
[data addFrame:uiKitClickableFrame];
}
}
// 从属性中获取到创建属性字符串使用CFAttributedStringSetAttribute设置的delegate值
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
if (!delegate) {
continue;
}
// CTRunDelegateGetRefCon方法从delegate中获取使用CTRunDelegateCreate初始时候设置的元数据
NSDictionary *metaData = (NSDictionary *)CTRunDelegateGetRefCon(delegate);
if (!metaData) {
continue;
}
// 找到代理则开始计算图片位置信息
CGFloat ascent;
CGFloat desent;
// 可以直接从metaData获取到图片的宽度和高度信息
CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
// CTLineGetOffsetForStringIndex获取CTRun的起始位置
CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
CGFloat yOffset = lineOrigins[i].y;
// 更新ImageItem对象的位置
if (imageIndex < self.images.count) {
YTImageItem *imageItem = self.images[imageIndex];
imageItem.frame = CGRectMake(xOffset, yOffset, width, ascent + desent);
imageIndex ++;
}
}
}
}
点击效果处理
上面的步骤以及处理好数据了,点击效果效果只要判断点击位置是否存在特殊内容,如果有获取特殊内容的所有CTRun的Frame,添加一个覆盖图层高亮显示就行了
// MARK: - Gesture
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = event.allTouches.anyObject;
CGPoint point = [touch locationInView:touch.view];
YTBaseDataItem *clickedItem = [self.data itemAtClickedPoint:point];
self.clickedItem = clickedItem;
NSLog(@"clickedItem = %@", clickedItem);
if (clickedItem) {
[self addClickedCoverWithItem:clickedItem];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
!self.clickedItem.clickActionHandler ?: self.clickedItem.clickActionHandler(_clickedItem);
self.clickedItem = nil;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self removeClickedCoverView];
});
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
self.clickedItem = nil;
[self touchesEnded:touches withEvent:event];
}
// MARK: - Helper
- (void)addClickedCoverWithItem:(YTBaseDataItem *)item {
for (NSValue *frameValue in item.frames) {
CGRect clickedPartFrame = frameValue.CGRectValue;
UIView *coverView = [[UIView alloc] initWithFrame:clickedPartFrame];
coverView.tag = COVER_TAG;
coverView.backgroundColor = [UIColor colorWithRed:0.3 green:1 blue:1 alpha:0.3];
coverView.layer.cornerRadius = 3;
[self addSubview:coverView];
}
}
- (void)removeClickedCoverView {
for (UIView *subView in self.subviews) {
if (subView.tag == COVER_TAG) {
[subView removeFromSuperview];
}
}
}
外部接口改善
YTDrawView
类是一个UIView的子类,负责内容的设置,以及最终的绘制,以下是YTDrawView
类中提供的几个设置内容的公开方法
/**
添加自定义的字符串并且设置字符串属性
@param string 字符串
@param attributes 字符串的属性
@param clickActionHandler 点击事件,暂时没效果 TODO
*/
- (void)addString:(NSString *)string attributes:(NSDictionary *)attributes clickActionHandler:(ClickActionHandler)clickActionHandler;
/**
添加链接
@param link 链接的地址
@param clickActionHandler 链接点击事件
*/
- (void)addLink:(NSString *)link clickActionHandler:(ClickActionHandler)clickActionHandler;
/**
添加图片
@param image 图片
@param size 图片大小
@param clickActionHandler 图片点击事件
*/
- (void)addImage:(UIImage *)image size:(CGSize)size clickActionHandler:(ClickActionHandler)clickActionHandler;
在这里YTDrawView
类相当于一个中介者,最终是把事情转交给YTRichContentData
类来做
// MARK: - Public
- (void)addString:(NSString *)string attributes:(NSDictionary *)attributes clickActionHandler:(ClickActionHandler)clickActionHandler {
[self.data addString:string attributes:attributes clickActionHandler:clickActionHandler];
}
- (void)addLink:(NSString *)link clickActionHandler:(ClickActionHandler)clickActionHandler {
[self.data addLink:link clickActionHandler:clickActionHandler];
}
- (void)addImage:(UIImage *)image size:(CGSize)size clickActionHandler:(ClickActionHandler)clickActionHandler {
[self.data addImage:image size:size clickActionHandler:clickActionHandler];
}
YTRichContentData
类专门处理和数据有关的事情,当YTDrawView
类需要显示,从YTRichContentData
类中获取数据,进行渲染绘制即可,这样职责就比较清楚明了,符合SRP原则,绘制需要修改就在YTDrawView
类中做修改,数据处理需要修改就在YTRichContentData
类修改即可。
- (void)addString:(NSString *)string attributes:(NSDictionary *)attributes clickActionHandler:(ClickActionHandler)clickActionHandler {
YTTextItem *textItem = [YTTextItem new];
textItem.content = string;
NSAttributedString *textAttributeString = [[NSAttributedString alloc] initWithString:textItem.content attributes:attributes];
[self.attributeString appendAttributedString:textAttributeString];
}
- (void)addLink:(NSString *)link clickActionHandler:(ClickActionHandler)clickActionHandler {
YTLinkItem *linkItem = [YTLinkItem new];
linkItem.link = link;
linkItem.clickActionHandler = clickActionHandler;
[self.links addObject:linkItem];
[self.attributeString appendAttributedString:[self linkAttributeStringWithLinkItem:linkItem]];
}
- (void)addImage:(UIImage *)image size:(CGSize)size clickActionHandler:(ClickActionHandler)clickActionHandler {
YTImageItem *imageItem = [YTImageItem new];
imageItem.image = image;
imageItem.clickActionHandler = clickActionHandler;
[self.images addObject:imageItem];
NSAttributedString *imageAttributeString = [self imageAttributeStringWithImageItem:imageItem size:size];
[self.attributeString appendAttributedString:imageAttributeString];
}