一些详细知识iOS技术资料iOS 成长之路

iOS 中 UILabel 文字部分点击,部分下划线

2017-04-06  本文已影响2851人  天空中的球

项目中突然有一个需求:一段文字中包含下划线的文字,并且包含点击事件。网上已经有 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)];

注意系统自带的:

typedef CF_OPTIONS(int32_t, CTUnderlineStyle) {
    kCTUnderlineStyleNone           = 0x00,
    kCTUnderlineStyleSingle         = 0x01,
    kCTUnderlineStyleThick          = 0x02,
    kCTUnderlineStyleDouble         = 0x09
};
CTUnderlineStyle
typedef CF_OPTIONS(int32_t, CTUnderlineStyleModifiers) {
    kCTUnderlinePatternSolid        = 0x0000,  
    kCTUnderlinePatternDot          = 0x0100, 
    kCTUnderlinePatternDash         = 0x0200, 
    kCTUnderlinePatternDashDot      = 0x0300, 
    kCTUnderlinePatternDashDotDot   = 0x0400 
};
CTUnderlineStyleModifiers
2-3、 如何确定点击事件

再次细分,可以理解为:

此处主要的是如何确定点击的点,是否我们需要的位置。此处添加点击事件之后,获取其位置,然后通过 Ponit判断对应的 attributedStringRect,

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;
}

通过上述思路可以了解到 全部文字的时候,可以很好的获取到每一个 subAttributedStringCGRect, 当然如果涉及到 图片这块的,还会涉及到 CTRunDelegateCallbacks:

typedef struct
{
    CFIndex                         version;
    CTRunDelegateDeallocateCallback dealloc; 
    CTRunDelegateGetAscentCallback  getAscent;  
    CTRunDelegateGetDescentCallback getDescent; 
    CTRunDelegateGetWidthCallback   getWidth; 
} CTRunDelegateCallbacks;

这个是为图片设置CTRunDelegate,delegate 决定留给图片的空间大小 ,这块先粗略的了解下。

** 对于此处笔记先重点还是:** 如何 获取到
NSMutableAttributedString 每一个子NSMutableAttributedStringCGRect 才是重点。

通过对上述三个问题的自我反问,大致有点理解了。但其中对于CoreText 某些的知识点,还是不熟悉的:

所以下次的学习笔记决定在这块让自己有新的认识,这次笔记就先到这里,如果上述的记录有问题或疑惑,欢迎告知。

上一篇下一篇

猜你喜欢

热点阅读