CoreText進階(三)-內容高亮和事件處理

本文的內容主要見到的是如何使用CoreText設置高亮的內容的特殊效果,好比帶有特殊顏色和下劃線的連接。以及這些高亮內容的點擊效果和點擊事件處理git

其它文章:
CoreText入門(一)-文本繪製
CoreText入門(二)-繪製圖片
CoreText進階(三)-事件處理
CoreText進階(四)-文字行數限制和顯示更多
CoreText進階(五)- 文字排版樣式和效果
CoreText進階(六)-內容大小計算和自動佈局
CoreText進階(七)-添加自定義View和對其數組

效果

Demo:CoreTextDemoapp

單行內容點擊效果
單行內容點擊效果佈局

圖片點擊效果ui

圖片點擊效果
 多行內容點擊效果
多行內容點擊效果spa

點擊事件處理

點擊事件的處理基本思路就是使用CTFrame對象獲取到全部的CTRun對象,遍歷CTRun對象,判斷CTRun位置的元素是否能夠點擊,須要以及幾個步驟.net

  • 給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的位置而且把保存的代碼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];
}
相關文章
相關標籤/搜索