(一一一)图文混排基础 -利用正则分割和拼接属性字符串

很多时候需要用到图文混排,例如聊天气泡中的表情,空间、微博中的表情,例如下图:

红心和文字在一起。

比较复杂的情况是表情夹杂在文字之间。

要实现这种功能,首先要介绍iOS中用于显示属性文字的类。

用于文字显示的类除了text属性之外,还有attributedText属性,这个属性是NSAttributedString类型,通过这个属性可以实现不同文字的不同字体、颜色甚至把图片作为文字显示的功能。

下面介绍这个字符串的使用。


以一条微博内容为应用场景,介绍如何从中找出表情、话题等内容,其中表情替换成表情图片,话题等高亮显示。

这里用到的内容主要有:正则表达式、NSAttributedString、NSTextAttachment等知识。

【正则表达式】

正则表达式在上一节(一一〇)正则表达式的基本使用与RegexKitLite的使用中有介绍,主要是为了找出所有特殊位置和非特殊位置。

NSAttributedString

这是一种能够对特定范围的文字设置属性、显示图片等功能。

下面介绍通过普通字符串初始化NSAttributedString,并且把其中的表情([<表情名称>])、话题(#<话题内容>#)、URL全部高亮的方法。

①通过微博字符串text初始化一个属性字符串。

 NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text];
②指定匹配规则。

 NSString *emotionPattern = @"\\[[a-zA-Z\\u4e00-\\u9fa5]+\\]";
 NSString *topicPattern = @"#[0-9a-zA-Z\\u4e00-\\u9fa5]+#";
 NSString *urlPattern = @"[a-zA-z]+://[^\\s]*";
 NSString *pattern = [NSString stringWithFormat:@"%@|%@|%@",emotionPattern,topicPattern,urlPattern];
③对匹配到的范围进行高亮,只需要调用NSMutableAttributedString的addAttribute:::属性对特定范围的文字设置颜色属性。

 [text enumerateStringsMatchedByRegex:pattern usingBlock:^(NSInteger captureCount, NSString *const __unsafe_unretained *capturedStrings, const NSRange *capturedRanges, volatile BOOL *const stop) {
        
   [attributedText addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:*capturedRanges];
        
 }];

通过这样的方法,所有匹配到的范围都会被标红。

④设置控件的attributedText属性为上面创建的attributedText值即可。


NSTextAttachment

NSTextAttachment可以设置image和bounds来指定图片和尺寸,并且可以包装成NSAttributedString,这是图文混排的基础。

下面介绍创建一个包含小图片的NSTextAttachment,并且用NSAttributedString包装的方法。

调整bounds的x、y可以修正图片在字符串中的位置。

 NSTextAttachment *attach = [[NSTextAttachment alloc] init];
 attach.image = [UIImage imageNamed:@"avatar_vgirl"];
 attach.bounds = CGRectMake(0, -3, 15, 15);
 NSAttributedString *emotionStr = [NSAttributedString attributedStringWithAttachment:attach];

【将字符串中的表情部分替换为表情图片】

为了简单,把所有表情位置的内容都替换为一张图片avatar_vgirl。

有一个自然的思路是调用attributedText的replace方法,把表情替换成图片(NSAttributedString包装的NSTextAttachment)。但是这个方法是有问题的。

例如下面的句子:

今天发送了[笑cry]一件很有意思的事情[笑cry]。

替换第一个表情[笑cry]为图片时,字符串的长度可能会发生变化,此时再处理第二个表情时计算出的位置可能就是错误的,因此应该先找到所有的表情位置然后统一替换。

因此我们可以用RegexKitLite的两个方法,分别找出匹配的和未匹配的,把他们存起来,然后按照位置的先后排序,最后按顺序拼接,拼接时对于表情换成图片,其他特殊字符高亮,余下的正常显示。

因为我们要把文字和范围全部放入数组,因此应该定义一个模型,为了方便起见,设置两个成员用于存储当前部分是不是表情、是不是特殊内容。

模型的代码如下:

@interface TextSegment : NSObject

@property (nonatomic, copy) NSString *text;
@property (nonatomic, assign) NSRange range;
@property (nonatomic, assign, getter=isSpecial) BOOL special;
@property (nonatomic, assign, getter=isEmotion) BOOL emotion;

@end

假设text是微博的全部内容,下面的代码实现把特殊内容和非特殊内容全部放入数组,并且判断是否是表情,表情的特点是有[ ],利用hasPrefix:和hasSufix:判断。

    NSString *emotionPattern = @"\\[[a-zA-Z\\u4e00-\\u9fa5]+\\]";
    NSString *topicPattern = @"#[0-9a-zA-Z\\u4e00-\\u9fa5]+#";
    NSString *urlPattern = @"[a-zA-z]+://[^\\s]*";
    NSString *pattern = [NSString stringWithFormat:@"%@|%@|%@",emotionPattern,topicPattern,urlPattern];
    
    // 把[表情]替换成attachment图片,不能用replace和insert,因为会改变后面的相对位置,应该先拿到所有位置,最后再统一修改。
    // 应该打散特殊部分和非特殊部分,然后拼接。
    NSMutableArray *parts = [NSMutableArray array];
    [text enumerateStringsMatchedByRegex:pattern usingBlock:^(NSInteger captureCount, NSString *const __unsafe_unretained *capturedStrings, const NSRange *capturedRanges, volatile BOOL *const stop) {
        
        if ((*capturedRanges).length == 0) return;
        
        TextSegment *seg = [[TextSegment alloc] init];
        seg.text = *capturedStrings;
        seg.range = *capturedRanges;
        seg.special = YES;
        
        seg.emotion = [seg.text hasPrefix:@"["] && [seg.text hasSuffix:@"]"];
        
        [parts addObject:seg];
        
    }];
    
    [text enumerateStringsSeparatedByRegex:pattern usingBlock:^(NSInteger captureCount, NSString *const __unsafe_unretained *capturedStrings, const NSRange *capturedRanges, volatile BOOL *const stop) {
        
        if ((*capturedRanges).length == 0) return;
       
        TextSegment *seg = [[TextSegment alloc] init];
        seg.text = *capturedStrings;
        seg.range = *capturedRanges;
        seg.special = NO;
        [parts addObject:seg];
        
    }];
通过上面的代码,我们把所有的文字部分都放入了parts数组中,为了拼接方便,我们应该按照位置的起始排序,从前到后依次拼接。

这就需要对parts数组依据模型的range.location属性排序,比较常用的是根据block排序。

block传入两个数组中的对象obj1、obj2,要求返回排序规则NSOrderedAscending、NSOrderedSame、NSOrderedDescending。

NSOrderedAscending指的是obj1<obj2,系统默认按照升序排序,因此为了实现升序,发现obj1<obj2应该返回NSOrderedAscending。

 [parts sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        
        TextSegment *ts1 = obj1;
        TextSegment *ts2 = obj2;
        
        // Descending指的是obj1>obj2
        // Ascending指的是obj1<obj2
        // 要实现升序,按照上面的规则返回。

        // 系统默认按照升序排列。
        if (ts1.range.location < ts2.range.location) {
            return NSOrderedAscending;
        }
        
        return NSOrderedDescending;
        
 }];

接下来只需要从前到后拼接一个新创建的NSAttributedString,根据内容的不同拼接不同的内容。

 NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] init];
 NSInteger cnt = parts.count;
 for (NSInteger i = 0; i < cnt; i++) {
      TextSegment *ts = parts[i];
      if (ts.isEmotion) {
          NSTextAttachment *attach = [[NSTextAttachment alloc] init];
          attach.image = [UIImage imageNamed:@"avatar_vgirl"];
          attach.bounds = CGRectMake(0, -3, 15, 15);
          NSAttributedString *emotionStr = [NSAttributedString attributedStringWithAttachment:attach];
          [attributedText appendAttributedString:emotionStr];
      }else if(ts.isSpecial){
          NSAttributedString *special = [[NSAttributedString alloc] initWithString:ts.text attributes:@{NSForegroundColorAttributeName:[UIColor redColor]}];
          [attributedText appendAttributedString:special];
      }else{
          [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:ts.text]];
      }
 }
最后把这个attributedText设置到控件上。


【计算NSAttributedString的尺寸】

①之前的text,计算尺寸的代码如下:需要指定字体和范围限制,一般是限制宽度,高度不限制(设置为MAXFLOAT),这样可以计算出正确的多行尺寸。

 CGSize contentSize = [text sizeWithFont:ContentFont constrainedToSize:CGSizeMake(MaxW, MaxH)]
②对于NSAttributedString,也有方法用于计算尺寸,注意计算之前必须为attributedText设定字体

 [attributedText addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:14] range:NSMakeRange(0, attributedText.length)];
然后调用boundingRectWithSize:::计算尺寸,注意options必须选择options:NSStringDrawingUsesLineFragmentOrigin才能得到正确尺寸。

 CGSize contentSize = [attributedText boundingRectWithSize:CGSizeMake(maxWidth, maxHeight) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;

posted on 2015-08-07 23:36  张大大123  阅读(285)  评论(0编辑  收藏  举报

导航