(一一一)图文混排基础 -利用正则分割和拼接属性字符串
很多时候需要用到图文混排,例如聊天气泡中的表情,空间、微博中的表情,例如下图:
红心和文字在一起。
比较复杂的情况是表情夹杂在文字之间。
要实现这种功能,首先要介绍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;