iOS给UILabel添加点击事件

释放双眼,带上耳机,听听看~!

前言:笔者最近需要实现给UILabel中的链接添加点击事件的功能。使用so.com查了下,发现TTTAttributedLabel的封装程度比较好。整理了TTTAttributedLabel的基本使用,并部分实现了。

TTTAttributedLabel的基本使用

TTTAttributedLabel.hTTTAttributedLabel.m放到项目中

遵守TTTAttributedLabelDelegate协议

// 遵守TTTAttributedLabelDelegate协议
@interface ViewController () <TTTAttributedLabelDelegate>
复制代码

创建TTTAttributedLabel实例及相应配置

创建TTTAttributedLabel实例,添加相应的配置。

- (void)setupTTTAttributedLabel {
    
    TTTAttributedLabel *attriLabel = [[TTTAttributedLabel alloc] initWithFrame:CGRectZero];
    attriLabel.font = [UIFont systemFontOfSize:32.0];
    attriLabel.numberOfLines = 0;
    // Automatically detect links when the label text is subsequently changed
    attriLabel.enabledTextCheckingTypes = NSTextCheckingTypeLink;
    // Delegate methods are called when the user taps on a link (see `TTTAttributedLabelDelegate` protocol)
    attriLabel.delegate = self;
    // Repository URL will be automatically detected and linked
    attriLabel.text = @"Fork me on GitHub! (https://github.com/mattt/TTTAttributedLabel/)";
    NSRange range = [attriLabel.text rangeOfString:@"me"];
    // Embedding a custom link in a substring
    [attriLabel addLinkToURL:[NSURL URLWithString:@"http://github.com/mattt/"] withRange:range];
    [self.view addSubview:attriLabel];
    attriLabel.frame = CGRectMake(20.0, 100.0, 300.0, 200.0);
}

实现TTTAttributedLabelDelegate代理方法

在如下代理方法中查看当前点击的链接。

//! 实现代理方法
- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithURL:(NSURL *)url {
    
    NSLog(@"url信息:%@", url);
}

TTTAttributedLabel部分实现

设置attriLabel.text = @"Fork me on GitHub! (https://github.com/mattt/TTTAttributedLabel/)";查看了TTTAttributedLabel的大概实现流程。

设置TTTAttributedLabel用户交互可用。

self.userInteractionEnabled = YES;

TTTAttributedLabel链接指定了替代链接样式

TTTAttributedLabel为链接指定了替代链接样式为蓝色和带下划线。

相关代码为:

NSMutableDictionary *mutableLinkAttributes = [NSMutableDictionary dictionary];
[mutableLinkAttributes setObject:[NSNumber numberWithBool:YES] forKey:(NSString *)kCTUnderlineStyleAttributeName];
if ([NSMutableParagraphStyle class]) {
    [mutableLinkAttributes setObject:[UIColor blueColor] forKey:(NSString *)kCTForegroundColorAttributeName];
} else {
    [mutableLinkAttributes setObject:(__bridge id)[[UIColor blueColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
}
self.linkAttributes = [NSDictionary dictionaryWithDictionary:mutableLinkAttributes];

使用NSDataDetector检测label.text中的链接

NSArray *results = [dataDetector matchesInString:[(NSAttributedString *)text string] options:0 range:NSMakeRange(0, [(NSAttributedString *)text length])];

NSArray<NSTextCheckingResult *> *类型的results 副本中会有label中文本的链接信息。

<__NSArrayM 0x2830a6310>(
<NSLinkCheckingResult: 0x283ed27c0>{20, 44}{https://github.com/mattt/TTTAttributedLabel/}
)

// label.text中的URL
(lldb) po ((NSLinkCheckingResult *)[results lastObject]).URL
https://github.com/mattt/TTTAttributedLabel/

// label.text中的URL 的range
(lldb) po ((NSLinkCheckingResult *)[results lastObject]).range
location=20, length=44

“自动检测”链接

当我们设置了attriLabel.text = @"Fork me on GitHub! (https://github.com/mattt/TTTAttributedLabel/)";时,会发现https://github.com/mattt/TTTAttributedLabel/自动转换链接形式。自动检测出label.text中的文本中有url信息是依次在- (NSArray *)addLinksWithTextCheckingResults:(NSArray *)results attributes:(NSDictionary *)attributes- (void)addLinks:(NSArray *)links的实现中做的处理。

  • 首先通过在- (NSArray *)addLinksWithTextCheckingResults:(NSArray *)results attributes:(NSDictionary *)attributes方法中封装链接文本属性信息媒介TTTAttributedLabelLink
  • 再通过在- (void)addLinks:(NSArray *)links方法中根据TTTAttributedLabelLink传递过来的文本属性及URL位置信息,设置label.attributedText以达到能够自动检测出label.texturl的目的。

另外我们自行添加addLinkToURl

- (void)addLinks:(NSArray *)links {
    ...
    self.attributedText = mutableAttributedString;
    ...
}

添加链接到指定 range

[attriLabel addLinkToURL:[NSURL URLWithString:@"http://github.com/mattt/"] withRange:range];为例。可以发现TTTAttributedLabel内部实现是

[self addLinkWithTextCheckingResult:[NSTextCheckingResult linkCheckingResultWithRange:range URL:url]]; 直到

[self addLinkWithTextCheckingResult:result attributes:self.linkAttributes]; 就和上述的“自动检测”链接的内容是一样的。

点击标签的链接文字后,查找到对应 url

这部分内容主要分为touchesBegan 方法中查找点击的位置的链接,touchesMoved方法中比对触摸到标签上的位置移动后,当前位置的链接和touchesBegan方法中找到的链接是否一样,最后在touchesEnded中把点击的链接以为块和代理的方式传递出去。

下边简单说明下笔者查看touchesBegan方法中查找点击链接的内容。

  • 获取到当前点击的点
[touch locationInView:self]

  • - (TTTAttributedLabelLink *)linkAtPoint:(CGPoint)point方法中获取到当前点链接

首先在- (CFIndex)characterIndexAtPoint:(CGPoint)p方法中找到点击的位置的字符的索引,然后通过- (TTTAttributedLabelLink *)linkAtCharacterIndex:(CFIndex)idx方法中找到对应的字符的索引的TTTAttributedLabelLink实例(其中包含当前点击点的链接信息)

相应的代码,详情见 TTTAttributedLabel

[self linkAtPoint:[touch locationInView:self]];
TTTAttributedLabelLink *result = [self linkAtCharacterIndex:[self characterIndexAtPoint:point]];

获取到当前点击位置的字符的索引

笔者感觉其中难理解的地方为获取到当前点击位置的字符的索引。相关内容如下:

将当前点的坐标转换为对应的UILabel中的文字坐标转换为针对于UILabel自身坐标系的点坐标;

p = CGPointMake(p.x - textRect.origin.x, p.y - textRect.origin.y);

另一个是把iOS做左上角为原点坐标转换为CT坐标系的左下角为原点坐标的调整。

p = CGPointMake(p.x, textRect.size.height - p.y);
  • 根据当前标签相对自身坐标系frame和属性链接创建CT坐标系所需的frame
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, textRect);
    CTFrameRef frame = CTFramesetterCreateFrame([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), path, NULL);

  • 确定UILabel当前在CT坐标系显示占用的行数 CFArrayGetCount(lines)
NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);

  • 遍历CT坐标中文字的每一行,找到当前点击点所在的行,计算出点击点相对于当前行的坐标,并计算出当前索引。
CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
idx = CTLineGetStringIndexForPosition(line, relativePoint);

其中还有ascent(字形最高点到baseline的推荐距离)和下降(字形最低点到baseline的推荐距离)相关的内容等。笔者不了解,有兴趣的话,可以查看CoreText相关内容,如深入理解Core Text投放引擎

	CFIndex idx = NSNotFound;	

    CGPoint lineOrigins[numberOfLines];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);

    for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
        CGPoint lineOrigin = lineOrigins[lineIndex];
        CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);

        // Get bounding information of line
        CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
        CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent);
        CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent);

        // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter
        CGFloat flushFactor = TTTFlushFactorForTextAlignment(self.textAlignment);
        CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, textRect.size.width);
        lineOrigin.x = penOffset;

        // Check if we've already passed the line
        if (p.y > yMax) {
            break;
        }
        // Check if the point is within this line vertically
        if (p.y >= yMin) {
            // Check if the point is within this line horizontally
            if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
                // Convert CT coordinates to line-relative coordinates
                CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
                idx = CTLineGetStringIndexForPosition(line, relativePoint);
                break;
            }
        }
    }
复制代码

本文说明了TTTAttributedLabel的基本使用及部分实现。有兴趣的读者请下载TTTAttributedLabel查看详情。

 

人已赞赏
iOS文章

iOS UIWebView加载HTML标签 适配字体,图片和文字大小

2019-12-10 19:38:36

iOS文章

Profile doesn't include the beta-reports-active entitlement.

2019-12-11 11:29:43

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索