实现过程1 今天来谈谈类似于新浪微博话题功能的简单实现,当文字是”#话题#”这种格式时,该文字字体得变颜色。个人觉得,这种问题的处理方式可以是,监听用户输入的信息,如果遇到有”#”号输入或删除时,再处理看是否需要改变字体颜色。于是我就按照这种思路写了一段改变颜色的代码,它就是是遍历textview.text,然后在将两个”#”号之间有文字的字体设置颜色,不会玩正则,所以这个方法比较蠢。O(∩_∩)O~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 /** * 设置textview字体属性 */ - (void)setTextViewAttributed { NSMutableArray *indexArray = [NSMutableArray array]; for (NSInteger i = 0; i < self.topicTextView.text.length; i++) { NSString *indexString = [self.topicTextView.text substringWithRange:NSMakeRange(i, 1)]; if ([indexString isEqualToString:topicString]) { [indexArray addObject:@(i)]; } } // reset NSMutableAttributedString *aText = [[NSMutableAttributedString alloc] initWithString:self.topicTextView.text]; self.topicTextView.attributedText = aText; self.topicTextView.font = [UIFont systemFontOfSize:16.0]; // change if (indexArray.count > 1) { NSMutableAttributedString *aText = [[NSMutableAttributedString alloc] initWithString:self.topicTextView.text]; for (NSInteger i = 0; i < indexArray.count; i++) { NSInteger index1 = [indexArray[i] integerValue]; NSInteger index2 = 0; if ((i + 1) < indexArray.count) { index2 = [indexArray[i + 1] integerValue]; } if (index2 - index1 > 1) { // 多余中间有值才显示 [aText setAttributes:@{ NSForegroundColorAttributeName: TopicColor } range:NSMakeRange(index1, index2 - index1 + 1)]; ++i; } } self.topicTextView.attributedText = aText; self.topicTextView.font = [UIFont systemFontOfSize:16.0]; } }
实现过程2 这个是我一厢情愿写的demo。因为现实并不是这样子的,客户端的话题只能从服务端获取,每个话题都是有标识的,因为不能让用户随心所欲的创建话题撒!所以呢,用户自己手动输入”#”号,然并卵。如果是这样子的话,那我只有三个问题要解决了:
改变话题字符串颜色;
光标不能移动到话题字符串中间,当用户光标移动至话题后面时,用户第一次点击键盘删除按钮,其实是选中这个话题的,再一次点击键盘删除按钮时,才会删除这个话题字符串;
上传至服务器的时候,并不是上传#话题#,而是上传协议好的字符串,就是能让服务端能够识别这个话题;
在实现过程中,我以AttributedString的颜色值为基准,用几个正则为查找工具,结合UITextView的三个代理方法。
1 2 3 4 5 6 7 8 9 /// Prior to replacing text, this method is called to give your delegate a chance to accept or reject the edits. If you do not implement this method, the return value defaults to YES. - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text; /// The text view calls this method in response to user-initiated changes to the text. This method is not called in response to programmatically initiated changes. Implementation of this method is optional. - (void)textViewDidChange:(UITextView *)textView; /// Implementation of this method is optional. You can use the selectedRange property of the text view to get the new selection. - (void)textViewDidChangeSelection:(UITextView *)textView;
这三个方法都不陌生吧,那么我的思路就简单了
shouldChangeTextInRange 代理方法中,第一,实现第一次选中,第二次删除功能;第二,实现插入话题后,需要改变其他字符串的初始颜色,得在这个方法里面做个标志。
textViewDidChange 代理方法中,实现 根据 shouldChangeTextInRange 方法中所得到的标志,设置字符串的初始颜色;
textViewDidChangeSelection 代理方法中,实现让光标不能移动到话题里面;
首先我定义了两个变量,插入了话题以后,继续在后面输入字符的话,字符颜色就跟话题颜色一样了。所以,我得用这两个变量来实现改变输入字符的初始颜色。
1 2 3 4 /// 改变Range @property (assign, nonatomic) NSRange changeRange; /// 是否改变 @property (assign, nonatomic) BOOL isChanged;
哦,对了,我还得用一个变量来记录上次光标所在的位置,因为话题字符串是不让它输入的。
1 2 /// 光标位置 @property (assign, nonatomic) NSInteger cursorLocation;
用户从其他界面选择好话题以后,它得插入到textview中啊:
1 2 3 4 5 NSString *insertText = [NSString stringWithFormat:@"#%@#", dict[KeyTopicName]]; [self.textView insertText:insertText]; NSMutableAttributedString *tmpAString = [[NSMutableAttributedString alloc] initWithAttributedString:self.textView.attributedText]; [tmpAString setAttributes:@{ NSForegroundColorAttributeName: TopicColor, NSFontAttributeName: DefaultSizeFont } range:NSMakeRange(self.textView.selectedRange.location - insertText.length, insertText.length)]; self.textView.attributedText = tmpAString;
然后我还得找到将用户所选择插入的话题位置啊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /** * 得到话题Range数组 * * @return return value description */ - (NSArray *)getTopicRangeArray:(NSAttributedString *)attributedString { NSAttributedString *traveAStr = attributedString ?: _textView.attributedText; __block NSMutableArray *rangeArray = [NSMutableArray array]; static NSRegularExpression *iExpression; iExpression = iExpression ?: [NSRegularExpression regularExpressionWithPattern:@"#(.*?)#" options:0 error:NULL]; [iExpression enumerateMatchesInString:traveAStr.string options:0 range:NSMakeRange(0, traveAStr.string.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { NSRange resultRange = result.range; NSDictionary *attributedDict = [traveAStr attributesAtIndex:resultRange.location effectiveRange:&resultRange]; if ([attributedDict[NSForegroundColorAttributeName] isEqual:TopicColor]) { [rangeArray addObject:NSStringFromRange(result.range)]; } }]; return rangeArray; }
那么,三个UITextView delegate方法里的代码就可以这么玩了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #pragma mark - UITextView Delegate - (void)textViewDidChangeSelection:(UITextView *)textView { NSArray *rangeArray = [self getTopicRangeArray:nil]; BOOL inRange = NO; for (NSInteger i = 0; i < rangeArray.count; i++) { NSRange range = NSRangeFromString(rangeArray[i]); if (textView.selectedRange.location > range.location && textView.selectedRange.location < range.location + range.length) { inRange = YES; break; } } if (inRange) { textView.selectedRange = NSMakeRange(self.cursorLocation, textView.selectedRange.length); return; } self.cursorLocation = textView.selectedRange.location; } - (void)textViewDidChange:(UITextView *)textView { if (_isChanged) { NSMutableAttributedString *tmpAString = [[NSMutableAttributedString alloc] initWithAttributedString:self.textView.attributedText]; [tmpAString setAttributes:@{ NSForegroundColorAttributeName: [UIColor blackColor], NSFontAttributeName: DefaultSizeFont } range:_changeRange]; _textView.attributedText = tmpAString; _isChanged = NO; } } - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { if ([text isEqualToString:@""]) { // 删除 NSArray *rangeArray = [self getTopicRangeArray:nil]; for (NSInteger i = 0; i < rangeArray.count; i++) { NSRange tmpRange = NSRangeFromString(rangeArray[i]); if ((range.location + range.length) == (tmpRange.location + tmpRange.length)) { if ([NSStringFromRange(tmpRange) isEqualToString:NSStringFromRange(textView.selectedRange)]) { // 第二次点击删除按钮 删除 return YES; } else { // 第一次点击删除按钮 选中 textView.selectedRange = tmpRange; return NO; } } } } else { // 增加 NSArray *rangeArray = [self getTopicRangeArray:nil]; if ([rangeArray count]) { for (NSInteger i = 0; i < rangeArray.count; i++) { NSRange tmpRange = NSRangeFromString(rangeArray[i]); if ((range.location + range.length) == (tmpRange.location + tmpRange.length) || !range.location) { _changeRange = NSMakeRange(range.location, text.length); _isChanged = YES; return YES; } } } else { // 话题在第一个删除后 重置text color if (!range.location) { _changeRange = NSMakeRange(range.location, text.length); _isChanged = YES; return YES; } } } return YES; }
好吧,通过以上方法,基本输入、删除操作功能是实现了。但是,上传貌似是个问题,因为我得上传跟服务端协议好的字符串格式啊;还有假如有一个聪明的用户,发现客户端和服务端的协议格式,他输入了那种格式的字符串,本着对服务端大哥的崇敬之心,我得将它转成”#话题#”格式啊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 /** * 获取上传时 textview text 字符串 * * @return <#return value description#> */ - (NSString *)getUploadString { NSMutableString *lastTopic = [NSMutableString string]; NSAttributedString *formatAString = [self formatAttributedString]; NSArray *rangeArray = [self getTopicRangeArray:formatAString]; NSInteger lastLocation = 0; for (NSInteger i = 0; i < rangeArray.count; i++) { // 转成协议字符串代码 } if (lastLocation < formatAString.string.length) { [lastTopic appendString:[formatAString.string substringFromIndex:lastLocation]]; } return lastTopic; } /** * 上传时候 将文本中不带topic color的 协议字符串 改成 "#话题#" 上传至服务器 * * @return <#return value description#> */ - (NSMutableAttributedString *)formatAttributedString { NSMutableAttributedString *tmpAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:_textView.attributedText]; static NSRegularExpression *iExpression; iExpression = iExpression ?: [NSRegularExpression regularExpressionWithPattern:@"获取协议字符串正则" options:0 error:NULL]; // 临时遍历的 topic数组 NSMutableArray *topicArray = [NSMutableArray array]; [topicArray addObjectsFromArray:[iExpression matchesInString:tmpAttributedString.string options:0 range:NSMakeRange(0, tmpAttributedString.string.length)]]; while (topicArray.count) { NSTextCheckingResult *result = [topicArray firstObject]; NSString *searchStr = [tmpAttributedString.string substringWithRange:result.range]; /** * 替换代码 */ [topicArray removeAllObjects]; [topicArray addObjectsFromArray:[iExpression matchesInString:tmpAttributedString.string options:0 range:NSMakeRange(0, tmpAttributedString.string.length)]]; } return tmpAttributedString; }
实现总结 文中,我只说了大致的实现方式以及核心功能代码,因为有的东西涉及到公司的。。。所以,都懂得哈! 在做这个功能过程中,我走了一些弯路。因为在上次博文中 ,我延时的发现AttributedString可以设置图片,所以我当时的想法是将”#话题#”这段文字转成image然后添加到textview的AttributedString中。 牛逼的代码都找好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /** * Text to Image * * @param text text description * * @return return value description */ - (UIImage *)imageFromText:(NSString *)text { NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new]; paragraph.lineBreakMode = NSLineBreakByCharWrapping; paragraph.alignment = NSTextAlignmentLeft; NSDictionary *attributeDict = @{NSFontAttributeName: [UIFont systemFontOfSize:14.0], NSForegroundColorAttributeName: [UIColor redColor], NSParagraphStyleAttributeName: paragraph}; CGSize textSize = [text sizeWithAttributes:attributeDict]; NSAttributedString *mutableString = [[NSAttributedString alloc] initWithString:text attributes:attributeDict]; UIGraphicsBeginImageContextWithOptions(textSize, NO, 0.0); [mutableString drawInRect:CGRectMake(0.0, 0.0, textSize.width, textSize.height)]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
可是。。。后面因为实现新浪微博,第一次删除选中,第二次删除删除的效果,这个思路被抛弃了。其实,这个思路当时还有两个问题,我还没有解决:
textview的AttributedString添加图片后,后面输入的文字和图片中的文字,centerY不相等,有偏差。
如果#话题#转为图片了,我得用什么方法去获取它在最终上传字符串中的位置,因为添加完成后,用户可以在任意位置删除、添加新的字符。
在解决这个问题后,我觉得潜意识挺重要的,因为当时这个功能要在三天之内完成嘛,到了第二天标准下班时间时我还没弄出来,晚上10点多下班后,还是没有进展。当时,就紧张了,因为我没有解决这个问题,那我就成为了问题了撒。穷则变嘛,后面机智的我果断决定用最熟悉的笨方法解决,下班后我就一直在思考这个用textview的代理方法该怎么搞,结果第三天早晨我醒的特别早,并且一醒来还在想那个问题,那睁开眼睛的情形宛如电视剧男主角失忆后第一次睁开眼,好帅的感觉,O(∩_∩)O哈哈~。洗完脸后,我深深的体会到了那句话:每天叫醒你的不只是闹钟,还有八阿哥!!!其实,就是把问题复杂化了!!!(⊙o⊙)…