iOS 中 UILabel 文字部分点击,部分下划线
项目中突然有一个需求:一段文字中包含下划线的文字,并且包含点击事件。网上已经有 iOS实现一段文字中部分有下划线,并且可以点击 这个实现方法啦。但是想到 UILabel 的也可以实现的。
一、 TYAttributedLabel 小试
记得之前搜罗过 github 上的富文本Label , 然后对比了下,用 TYAttributedLabel 小试了一下,实现下面这中类似效果:
效果图#import "ViewController.h"
#import "TYAttributedLabel.h"
@interface ViewController ()<TYAttributedLabelDelegate>
@property (nonatomic, strong) TYAttributedLabel *testLabel;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;
[self addTextAttributed];
}
- (void)addTextAttributed {
[self.view addSubview:self.testLabel];
// 规则声明
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]initWithString:@"I agree to myApp "];
[attributedString addAttributeTextColor:[UIColor blackColor]];
[attributedString addAttributeFont:[UIFont systemFontOfSize:15]];
[self.testLabel appendTextAttributedString:attributedString];
// 增加链接 Terms and Conditions
[self.testLabel appendLinkWithText:@"Terms and Conditions" linkFont:[UIFont systemFontOfSize:15] linkColor:[UIColor blueColor] linkData:@"https://www.baidu.com"];
// And
NSMutableAttributedString *attributedAndString = [[NSMutableAttributedString alloc]initWithString:@" and "];
[attributedAndString addAttributeTextColor:[UIColor blackColor]];
[attributedAndString addAttributeFont:[UIFont systemFontOfSize:15]];
[self.testLabel appendTextAttributedString:attributedAndString];
// 增加链接 Privacy Polices
[self.testLabel appendLinkWithText:@"Privacy Polices" linkFont:[UIFont systemFontOfSize:15] linkColor:[UIColor blueColor] linkData:@"https://www.google.com"];
[self.testLabel sizeToFit];
}
#pragma mark - Delegate
//TYAttributedLabelDelegate
- (void)attributedLabel:(TYAttributedLabel *)attributedLabel textStorageClicked:(id<TYTextStorageProtocol>)TextRun atPoint:(CGPoint)point {
if ([TextRun isKindOfClass:[TYLinkTextStorage class]]) {
NSString *linkStr = ((TYLinkTextStorage*)TextRun).linkData;
NSLog(@"linkStr === %@",linkStr);
}
}
#pragma mark - Getter
- (TYAttributedLabel *)testLabel {
if (!_testLabel) {
_testLabel = [[TYAttributedLabel alloc] initWithFrame:CGRectMake(20, 20, [UIScreen mainScreen].bounds.size.width - 40, 0)];
_testLabel.delegate = self;
_testLabel.highlightedLinkColor = [UIColor orangeColor];
}
return _testLabel;
}
@end
二、 TYAttributedLabel 学习
- 如何确定高度
- 如何画下滑线
- 如何确定点击事件
2-1、如何确定高度
通过调用 sizeToFit
, 再重调用 sizeThatFits
, 然后获取想要的高度:
- (void)sizeToFit
{
[super sizeToFit];
}
- (CGSize)sizeThatFits:(CGSize)size
{
return [self getSizeWithWidth:CGRectGetWidth(self.frame)];
}
这里要特别看下获取高度的方法,它这边也是从 TTTAttributedLabel 引用的。
// this code quote TTTAttributedLabel
static inline CGSize CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter, NSAttributedString *attributedString, CGSize size, NSUInteger numberOfLines) {
CFRange rangeToSize = CFRangeMake(0, (CFIndex)[attributedString length]);
CGSize constraints = CGSizeMake(size.width, MAXFLOAT);
if (numberOfLines > 0) {
// If the line count of the label more than 1, limit the range to size to the number of lines that have been set
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0.0f, 0.0f, constraints.width, MAXFLOAT));
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CFArrayRef lines = CTFrameGetLines(frame);
if (CFArrayGetCount(lines) > 0) {
NSInteger lastVisibleLineIndex = MIN((CFIndex)numberOfLines, CFArrayGetCount(lines)) - 1;
CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
}
CFRelease(frame);
CFRelease(path);
}
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, rangeToSize, NULL, constraints, NULL);
return CGSizeMake(ceil(suggestedSize.width), ceil(suggestedSize.height));
}
这一块具体的计算还有点懵,简单的来说就是对于 NSAttributedString
通过CTFramesetterRef
的相关换算来计算所占的 size
, 此处有待挖掘。
2-2、如何画下滑线
此处是直接通过 NSMutableAttributedString
的系统方法,只是感觉平常对CoreText
对这块了解太少了。
- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range
[attributedString addAttribute:(NSString *)kCTUnderlineStyleAttributeName
value:[NSNumber numberWithInt:(kCTUnderlineStyleSingle|kCTUnderlinePatternSolid)]
range:NSMakeRange(0, attributedString.length)];
注意系统自带的:
-
kCTUnderlineStyleAttributeName
字体下划线样式属性 必须是CFNumberRef对象,默为kCTUnderlineStyleNone 可以通过CTUnderlineStypleModifiers 进行修改下划线风格 -
kCTUnderlineStyleSingle
typedef CF_OPTIONS(int32_t, CTUnderlineStyle) {
kCTUnderlineStyleNone = 0x00,
kCTUnderlineStyleSingle = 0x01,
kCTUnderlineStyleThick = 0x02,
kCTUnderlineStyleDouble = 0x09
};
CTUnderlineStyle
- kCTUnderlinePatternSolid
typedef CF_OPTIONS(int32_t, CTUnderlineStyleModifiers) {
kCTUnderlinePatternSolid = 0x0000,
kCTUnderlinePatternDot = 0x0100,
kCTUnderlinePatternDash = 0x0200,
kCTUnderlinePatternDashDot = 0x0300,
kCTUnderlinePatternDashDotDot = 0x0400
};
CTUnderlineStyleModifiers
2-3、 如何确定点击事件
再次细分,可以理解为:
- 如何确定点击的点是否有反应的
- 如何找到点击的位置属于哪一块
- 如何将之前值通过代理传值过去
此处主要的是如何确定点击的点,是否我们需要的位置。此处添加点击事件之后,获取其位置,然后通过 Ponit
判断对应的 attributedString
的 Rect
,
CG_EXTERN bool CGRectContainsPoint(CGRect rect, CGPoint point)
从而做出响应判断。
下面是我用 伪代码 大致实现其思路,假装只有一行的实现:
伪代码所实现的效果- 注意是伪代码式例:
- (void)addLineTestLabel {
[self.view addSubview:self.testLineLabel];
self.testLineLabel.userInteractionEnabled = YES;
[self.testLineLabel addGestureRecognizer:self.tabGesture];
// 总的 attributedString
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]init];
// 实现 One 下划线
NSMutableAttributedString *attributedOneString = [[NSMutableAttributedString alloc]initWithString:@"One String"];
[attributedOneString addAttribute:(NSString *)kCTUnderlineStyleAttributeName
value:[NSNumber numberWithInt:(kCTUnderlineStyleSingle|kCTUnderlinePatternSolid)]
range:NSMakeRange(0, attributedOneString.length)];
[attributedOneString addAttribute:(NSString *)kCTUnderlineColorAttributeName
value:(id)[UIColor redColor].CGColor
range:NSMakeRange(0, attributedOneString.length)];
// 记录 One 位置,此处是模拟, 60 是指attributedOneString 的长度
CGRect runOneRect = CGRectMake(0, 0, 60,self.testLineLabel.frame.size.height);
[self.testDic setObject:@"https://www.testOne.com" forKey:[NSValue valueWithCGRect:runOneRect]];
// 实现 And
NSMutableAttributedString *attributedAndString = [[NSMutableAttributedString alloc]initWithString:@" And "];
// 实现 Two 下划线
NSMutableAttributedString *attributedTwoString = [[NSMutableAttributedString alloc]initWithString:@"Two String"];
[attributedTwoString addAttribute:(NSString *)kCTUnderlineStyleAttributeName
value:[NSNumber numberWithInt:(kCTUnderlineStyleSingle|kCTUnderlinePatternDot)]
range:NSMakeRange(0, attributedTwoString.length)];
// 记录 Two 位置,此处是模拟, 60 是指 attributedTwoString 的长度
CGRect runTwoRect = CGRectMake(self.testLineLabel.frame.size.width - 60, 0, attributedTwoString.length,self.testLineLabel.frame.size.height);
[self.testDic setObject:@"https://www.testTwo.com" forKey:[NSValue valueWithCGRect:runTwoRect]];
// 拼接起来
[attributedString appendAttributedString:attributedOneString];
[attributedString appendAttributedString:attributedAndString];
[attributedString appendAttributedString:attributedTwoString];
[attributedString setAttributes:@{NSForegroundColorAttributeName : [UIColor orangeColor]} range:NSMakeRange(attributedOneString.length, attributedAndString.length)];
self.testLineLabel.attributedText = attributedString;
}
- (void)tapGestureAction:(UITapGestureRecognizer *)tap {
CGPoint point = [tap locationInView:self.testLineLabel];
[self enumerateRunRectWithContainPoint:point successBlock:^(NSString *linkStr) {
NSLog(@"testLinkStr === %@",linkStr);
}];
}
- (BOOL)enumerateRunRectWithContainPoint:(CGPoint)point successBlock:(void (^)(NSString *linkStr))successBlock {
if (self.testDic.count < 1) {
return NO;
}
__block BOOL find = NO;
[self.testDic enumerateKeysAndObjectsUsingBlock:^(NSValue *keyRectValue, NSString *linkStr, BOOL * _Nonnull stop) {
CGRect rect = [keyRectValue CGRectValue];
if(CGRectContainsPoint(rect, point)){
find = YES;
*stop = YES;
if (successBlock) {
successBlock(linkStr);
}
}
}];
return find;
}
当然里面实现是复杂很多,考虑了各种情况的,其中最主要的是 如何获取 该attributedString
对应的位置:
- (void)saveTextRectWithAttributedString:(NSMutableAttributedString *)attributedString {
// 创建CTFramesetter
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
// 创建CTFrameRef
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, self.testLineLabel.frame.size.width, self.testLineLabel.frame.size.height));
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedString.length), path, NULL);
CFRelease(path);
// 释放内存
CFRelease(framesetter);
// 保存 run rect
[self getTextRectWithFrame:frameRef];
}
- (CGRect)getTextRectWithFrame:(CTFrameRef)frame
{
// 获取每行
CFArrayRef lines = CTFrameGetLines(frame);
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
CGFloat viewWidth = self.testLineLabel.frame.size.width;
NSInteger numberOfLines = self.testLineLabel.numberOfLines > 0 ? MIN(self.testLineLabel.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);;
CGRect runRect = CGRectZero;
// 获取每行有多少run
for (int i = 0; i < numberOfLines; i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGFloat lineAscent;
CGFloat lineDescent;
CGFloat lineLeading;
CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);
CFArrayRef runs = CTLineGetGlyphRuns(line);
// 获得每行的 run
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CGFloat runAscent;
CGFloat runDescent;
CGPoint lineOrigin = lineOrigins[i];
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
// 获取 value
NSDictionary* attributes = (NSDictionary*)CTRunGetAttributes(run);
id linkStr = [attributes objectForKey:@"testAttributedKey"];
// 开始获取 runRect
CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0,0), &runAscent, &runDescent, NULL);
if (viewWidth > 0 && runWidth > viewWidth) {
runWidth = viewWidth;
}
runRect = CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL), lineOrigin.y - runDescent, runWidth, runAscent + runDescent);
// 设置 此处符合某个自己设置的规则,则保存相应的值
if ([(NSString *)linkStr hasPrefix:@"http"]) {
[self.testDic setObject:linkStr forKey:[NSValue valueWithCGRect:runRect]];
}
}
}
return runRect;
}
通过上述思路可以了解到 全部文字的时候,可以很好的获取到每一个 subAttributedString
的 CGRect
, 当然如果涉及到 图片这块的,还会涉及到 CTRunDelegateCallbacks
:
typedef struct
{
CFIndex version;
CTRunDelegateDeallocateCallback dealloc;
CTRunDelegateGetAscentCallback getAscent;
CTRunDelegateGetDescentCallback getDescent;
CTRunDelegateGetWidthCallback getWidth;
} CTRunDelegateCallbacks;
这个是为图片设置CTRunDelegate
,delegate
决定留给图片的空间大小 ,这块先粗略的了解下。
** 对于此处笔记先重点还是:** 如何 获取到
NSMutableAttributedString
每一个子NSMutableAttributedString
的 CGRect
才是重点。
通过对上述三个问题的自我反问,大致有点理解了。但其中对于CoreText 某些的知识点,还是不熟悉的:
- CTFramesetterRef
- CTFrameRef
- CFArrayRef
- CTRunDelegateCallbacks
所以下次的学习笔记决定在这块让自己有新的认识,这次笔记就先到这里,如果上述的记录有问题或疑惑,欢迎告知。