本文的內容主要見到的是如何使用CoreText設置高亮的內容的特殊效果,好比帶有特殊顏色和下劃線的連接。以及這些高亮內容的點擊效果和點擊事件處理git
其它文章:
CoreText入門(一)-文本繪製
CoreText入門(二)-繪製圖片
CoreText進階(三)-事件處理
CoreText進階(四)-文字行數限制和顯示更多
CoreText進階(五)- 文字排版樣式和效果
CoreText進階(六)-內容大小計算和自動佈局
CoreText進階(七)-添加自定義View和對其數組
Demo:CoreTextDemoapp
單行內容點擊效果
 佈局
圖片點擊效果ui
 多行內容點擊效果
 spa
點擊事件的處理基本思路就是使用CTFrame對象獲取到全部的CTRun對象,遍歷CTRun對象,判斷CTRun位置的元素是否能夠點擊,須要以及幾個步驟.net
給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的位置而且把保存的代碼code
- (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<UITouch *> *)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<UITouch *> *)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<UITouch *> *)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]; }