美文网首页一些详细知识iOS技术资料iOS 成长之路
iOS 中 UILabel 文字部分点击,部分下划线

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

作者: 天空中的球 | 来源:发表于2017-04-06 22:51 被阅读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)];
    
    

    注意系统自带的:

    • 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判断对应的 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 某些的知识点,还是不熟悉的:

    • CTFramesetterRef
    • CTFrameRef
    • CFArrayRef
    • CTRunDelegateCallbacks

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

    相关文章

      网友评论

      本文标题:iOS 中 UILabel 文字部分点击,部分下划线

      本文链接:https://www.haomeiwen.com/subject/bpiiottx.html