本文的内容主要见到的是如何使用CoreText设置高亮的内容的特殊效果,比如带有特殊颜色和下划线的链接。以及这些高亮内容的点击效果和点击事件处理
其它文章:
效果
Demo:
单行内容点击效果
图片点击效果
 多行内容点击效果 点击事件处理
点击事件的处理基本思路就是使用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];}