IOS CoreText系列四:圖文混排之點擊事件

主要思路:基於-(void)touchesBegan:(NSSet<UITouch *> )touches withEvent:(UIEvent )event這個方法拿到當前點擊到的點,然後通過座標判斷這個點是否在某段文字上,如果在則觸發對應事件。

1、首先需要執行touchesBegan這個方法來做相應的判斷

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint location = [self systemPointFromScreenPoint:[touch locationInView:self]];
    //判斷點擊的是圖片還是文字
    if ([self checkIsClickOnImgWithPoint:location]) {
        return;
    }
    [self clickOnStringWithPoint:location];
}

2、點擊的是圖片做相應的處理,圖片的位置我們在圖文混排的時候會獲取到,這邊直接拿過來用,如果圖片有很多張的話,需要搞個循環來做判斷。

#pragma mark 點擊圖片
- (BOOL)checkIsClickOnImgWithPoint:(CGPoint)location
{
    if ([self isFrame:_imgFrm containsPoint:location]) {
        NSLog(@"你點擊到了圖片");
        return YES;
    }
    return NO;
}

- (BOOL)isFrame:(CGRect)frame containsPoint:(CGPoint)point
{
    return CGRectContainsPoint(frame, point);
}
3、點擊的是字符串,步驟如下:

/*
 點擊文字判斷的大致步驟:
 1.根據Frame拿到所有Line
 2.計算每個Line中在全文的range
 3.計算每個字對應line原點的X值
 4.比對對應line的origin求得字對應起點座標
 5.求得下一個字的橫座標和上一行的origin,結合起點座標得出字的座標範圍
 6.屏幕座標與drawRect座標轉換,判斷是否在範圍內
 */
#pragma mark 點擊字符串
- (void)clickOnStringWithPoint:(CGPoint)point
{
    //獲取所有CTLine
    NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrm);
    //初始化範圍數組
    CFRange ranges[lines.count];
    //初始化原點數組
    CGPoint origins[lines.count];
    //獲取所有CTLine的原點
    CTFrameGetLineOrigins(self.ctFrm, CFRangeMake(0, 0), origins);
    
    //獲取每個CTLine中包含的富文本在整串富文本中的範圍。將所有CTLine中字符串的範圍保存下來放入數組中。
    for (int i = 0; i < lines.count; i ++) {
        CTLineRef line = (__bridge CTLineRef)(lines[i]);
        CFRange range = CTLineGetStringRange(line);
        ranges[i] = range;
    }
    
    for (int i = 0;i < self.strLength; i ++) {
        long maxLoc;
        int lineNum;
        for (int j = 0; j < lines.count; j ++) {
            CFRange range = ranges[j];
            maxLoc = range.location + range.length - 1;
            if (i <= maxLoc) {
                lineNum = j;
                break;
            }
        }
        
        CTLineRef line = (__bridge CTLineRef)(lines[lineNum]);
        CGPoint origin = origins[lineNum];
        CGRect ctRunFrame = [self frameForCTRunWithIndex:i ctLine:line origin:origin];
        if ([self isFrame:ctRunFrame containsPoint:point]) {
            NSLog(@"您點擊到了第 %d 個字符,位於第 %d 行,然而他沒有響應事件。",i,lineNum + 1);
            
            return;
        }
    }
    NSLog(@"您沒有點擊到文字");
}

/**
 字符Frame計算

 @param index  索引
 @param line   索引字符所在CTLine
 @param origin line的起點

 @return 返回Frame
 */
- (CGRect)frameForCTRunWithIndex:(NSInteger)index ctLine:(CTLineRef)line origin:(CGPoint)origin
{
    //獲取字符起點相對於CTLine的原點的偏移量
    CGFloat offsetX = CTLineGetOffsetForStringIndex(line, index, NULL);
    //獲取下一個字符的偏移量,兩者之間即爲字符X範圍
    CGFloat offsetX2 = CTLineGetOffsetForStringIndex(line, index + 1, NULL);
    offsetX += origin.x;
    offsetX2 += origin.x;
    CGFloat offsetY = origin.y;
    CGFloat lineAscent,lineDescent;
    //獲取當前點擊的CTRun
    NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
    CTRunRef runCurrent;
    for (int k = 0; k < runs.count; k ++) {
        CTRunRef run = (__bridge CTRunRef)(runs[k]);
        CFRange range = CTRunGetStringRange(run);
        NSRange rangeOC = NSMakeRange(range.location, range.length);
        if ([self isIndex:index inRange:rangeOC]) {
            runCurrent = run;
            break;
        }
    }
    //獲得對應CTRun的尺寸信息
    CTRunGetTypographicBounds(runCurrent, CFRangeMake(0, 0), &lineAscent, &lineDescent, NULL);
    //計算當前點擊的CTRun高度
    CGFloat height = lineAscent + lineDescent;
    return CGRectMake(offsetX, offsetY, offsetX2 - offsetX, height);
}


/**
 範圍檢測

 @param index 索引
 @param range 範圍

 @return 範圍內返回yes,否則返回no
 */
- (BOOL)isIndex:(NSInteger)index inRange:(NSRange)range
{
    if ((index <= (range.location + range.length - 1)) && (index >= range.location)) {
        return YES;
    }
    return NO;
}

完整代碼如下:

.h文件

#import <UIKit/UIKit.h>

@interface HSCoreTextView : UIView

@end

.m文件

#import "HSCoreTextView.h"
#import <CoreText/CoreText.h>

@interface HSCoreTextView ()

@property (nonatomic,assign) CGRect imgFrm;

@property (nonatomic,assign) CTFrameRef ctFrm;

@property (nonatomic,assign) NSInteger strLength;

@end

@implementation HSCoreTextView


//圖片回調函數
static CGFloat ascentCallBacks(void *ref)
{
    //__bridge既是C的結構體轉換成OC對象時需要的一個修飾詞
    return [[(__bridge NSDictionary *)ref valueForKey:@"height"] floatValue];
}

static CGFloat descentCallBacks(void *ref)
{
    return 0;
}

static CGFloat widthCallBacks(void *ref)
{
    return [[(__bridge NSDictionary *)ref valueForKey:@"width"] floatValue];
}

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    
    //1.繪製上下文
    //1.1獲取繪製上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    //1.2.coreText 起初是爲OSX設計的,而OSX得座標原點是左下角,y軸正方向朝上。iOS中座標原點是左上角,y軸正方向向下。若不進行座標轉換,則文字從下開始,還是倒着的,因此需要設置以下屬性
    ////設置字形的變換矩陣爲不做圖形變換
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    //平移方法,將畫布向上平移一個屏幕高
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    //縮放方法,x軸縮放係數爲1,則不變,y軸縮放係數爲-1,則相當於以x軸爲軸旋轉180度
    CGContextScaleCTM(context, 1.0, -1.0);
    
    //2.設置圖片回調函數
    //2.1創建一個回調結構體,設置相關參數
    CTRunDelegateCallbacks callBacks;
    //memset將已開闢內存空間 callbacks 的首 n 個字節的值設爲值 0, 相當於對CTRunDelegateCallbacks內存空間初始化
    memset(&callBacks, 0, sizeof(CTRunDelegateCallbacks));
    //2.2設置回調版本,默認這個
    callBacks.version = kCTRunDelegateVersion1;
    //2.3設置圖片頂部距離基線的距離
    callBacks.getAscent = ascentCallBacks;
    //2.4設置圖片底部距離基線的距離
    callBacks.getDescent = descentCallBacks;
    //2.5設置圖片寬度
    callBacks.getWidth = widthCallBacks;
    //2.6創建一個代理
    NSDictionary *dicPic = @{@"height":@"60",@"width":@"60"};
    CTRunDelegateRef delegate = CTRunDelegateCreate(&callBacks, (__bridge void * _Nullable)(dicPic));

    //3.插入圖片
    //創建空白字符
    unichar placeHolder = 0xFFFC;
    NSString *placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
    NSMutableAttributedString *placeHolderMabString = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
    //給字符串中的範圍中字符串設置代理
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderMabString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
    
    //4.設置要顯示的文字
    NSMutableAttributedString *mabString = [[NSMutableAttributedString alloc] initWithString:@"\n這裏在測試圖文混排,\n我是富文本"];
    [mabString insertAttributedString:placeHolderMabString atIndex:12];
    
    //5.繪製文本
    //5.1.創建CTFramesetterRef
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mabString);
    
    //5.2.創建路徑
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    
    //5.3.創建CTFrame
    NSInteger length = mabString.length;
    self.strLength = length;
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, length), path, NULL);
    self.ctFrm = frame;
    CTFrameDraw(frame, context);
    
    //6.添加圖片並繪製
    UIImage *image = [UIImage imageNamed:@"icon-60"];
    CGRect imgFrm = [self calculateImageRectWithFrame:frame];
    self.imgFrm = imgFrm;
    CGContextDrawImage(context, imgFrm, image.CGImage);
    
    //7.釋放
    CFRelease(delegate);
//    CFRelease(frame);
    CFRelease(path);
    CFRelease(framesetter);
}

#pragma mark 計算圖片Frame
- (CGRect)calculateImageRectWithFrame:(CTFrameRef)frame
{
    //根據frame獲取需要繪製的線的數組
    NSArray *arrLines = (NSArray *)CTFrameGetLines(frame);
    NSInteger count = arrLines.count;
    CGPoint points[count];
    //獲取起始點位置
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
    
    for (int i = 0; i < count; i ++) {
        CTLineRef line = (__bridge CTLineRef)(arrLines[i]);
        //CTRun 或者叫做 Glyph Run,是一組共享想相同attributes(屬性)的字形的集合體
        NSArray *arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);
        for (int j = 0; j < arrGlyphRun.count; j ++) {
            CTRunRef run = (__bridge CTRunRef)(arrGlyphRun[j]);
            //獲取CTRun的屬性
            NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
            if (delegate == nil) {
                continue;
            }
            
            NSDictionary *dic = CTRunDelegateGetRefCon(delegate);
            if (![dic isKindOfClass:[NSDictionary class]]) {
                continue;
            }
            
            //獲取一個起點
            CGPoint point = points[i];
            //獲取上下距
            CGFloat ascent,desecent;
            //創建一個Frame
            CGRect boundsRun;
            boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desecent, NULL);
            boundsRun.size.height = ascent + desecent;
            //獲取偏移量
            CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            boundsRun.origin.x = point.x + xOffset;
            boundsRun.origin.y = point.y - desecent;
            //獲取繪製路徑
            CGPathRef path = CTFrameGetPath(frame);
            //獲取剪裁區域邊框
            CGRect colRect = CGPathGetBoundingBox(path);
            CGRect imageBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);
            
            return imageBounds;
        }
    }
    return CGRectZero;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint location = [self systemPointFromScreenPoint:[touch locationInView:self]];
    //判斷點擊的是圖片還是文字
    if ([self checkIsClickOnImgWithPoint:location]) {
        return;
    }
    [self clickOnStringWithPoint:location];
}

#pragma mark 轉化成屏幕座標
- (CGPoint)systemPointFromScreenPoint:(CGPoint)origin
{
    return CGPointMake(origin.x, self.bounds.size.height - origin.y);
}

#pragma mark 點擊圖片
- (BOOL)checkIsClickOnImgWithPoint:(CGPoint)location
{
    if ([self isFrame:_imgFrm containsPoint:location]) {
        NSLog(@"你點擊到了圖片");
        return YES;
    }
    return NO;
}

- (BOOL)isFrame:(CGRect)frame containsPoint:(CGPoint)point
{
    return CGRectContainsPoint(frame, point);
}

/*
 點擊文字判斷的大致步驟:
 1.根據Frame拿到所有Line
 2.計算每個Line中在全文的range
 3.計算每個字對應line原點的X值
 4.比對對應line的origin求得字對應起點座標
 5.求得下一個字的橫座標和上一行的origin,結合起點座標得出字的座標範圍
 6.屏幕座標與drawRect座標轉換,判斷是否在範圍內
 */
#pragma mark 點擊字符串
- (void)clickOnStringWithPoint:(CGPoint)point
{
    //獲取所有CTLine
    NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrm);
    //初始化範圍數組
    CFRange ranges[lines.count];
    //初始化原點數組
    CGPoint origins[lines.count];
    //獲取所有CTLine的原點
    CTFrameGetLineOrigins(self.ctFrm, CFRangeMake(0, 0), origins);
    
    //獲取每個CTLine中包含的富文本在整串富文本中的範圍。將所有CTLine中字符串的範圍保存下來放入數組中。
    for (int i = 0; i < lines.count; i ++) {
        CTLineRef line = (__bridge CTLineRef)(lines[i]);
        CFRange range = CTLineGetStringRange(line);
        ranges[i] = range;
    }
    
    for (int i = 0;i < self.strLength; i ++) {
        long maxLoc;
        int lineNum;
        for (int j = 0; j < lines.count; j ++) {
            CFRange range = ranges[j];
            maxLoc = range.location + range.length - 1;
            if (i <= maxLoc) {
                lineNum = j;
                break;
            }
        }
        
        CTLineRef line = (__bridge CTLineRef)(lines[lineNum]);
        CGPoint origin = origins[lineNum];
        CGRect ctRunFrame = [self frameForCTRunWithIndex:i ctLine:line origin:origin];
        if ([self isFrame:ctRunFrame containsPoint:point]) {
            NSLog(@"您點擊到了第 %d 個字符,位於第 %d 行,然而他沒有響應事件。",i,lineNum + 1);
            
            return;
        }
    }
    NSLog(@"您沒有點擊到文字");
}

/**
 字符Frame計算

 @param index  索引
 @param line   索引字符所在CTLine
 @param origin line的起點

 @return 返回Frame
 */
- (CGRect)frameForCTRunWithIndex:(NSInteger)index ctLine:(CTLineRef)line origin:(CGPoint)origin
{
    //獲取字符起點相對於CTLine的原點的偏移量
    CGFloat offsetX = CTLineGetOffsetForStringIndex(line, index, NULL);
    //獲取下一個字符的偏移量,兩者之間即爲字符X範圍
    CGFloat offsetX2 = CTLineGetOffsetForStringIndex(line, index + 1, NULL);
    offsetX += origin.x;
    offsetX2 += origin.x;
    CGFloat offsetY = origin.y;
    CGFloat lineAscent,lineDescent;
    //獲取當前點擊的CTRun
    NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
    CTRunRef runCurrent;
    for (int k = 0; k < runs.count; k ++) {
        CTRunRef run = (__bridge CTRunRef)(runs[k]);
        CFRange range = CTRunGetStringRange(run);
        NSRange rangeOC = NSMakeRange(range.location, range.length);
        if ([self isIndex:index inRange:rangeOC]) {
            runCurrent = run;
            break;
        }
    }
    //獲得對應CTRun的尺寸信息
    CTRunGetTypographicBounds(runCurrent, CFRangeMake(0, 0), &lineAscent, &lineDescent, NULL);
    //計算當前點擊的CTRun高度
    CGFloat height = lineAscent + lineDescent;
    return CGRectMake(offsetX, offsetY, offsetX2 - offsetX, height);
}


/**
 範圍檢測

 @param index 索引
 @param range 範圍

 @return 範圍內返回yes,否則返回no
 */
- (BOOL)isIndex:(NSInteger)index inRange:(NSRange)range
{
    if ((index <= (range.location + range.length - 1)) && (index >= range.location)) {
        return YES;
    }
    return NO;
}

- (void)dealloc
{
    CFRelease(_ctFrm);
}

@end






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章