本文共 12273 字,大约阅读时间需要 40 分钟。
之前项目需要展示富文本,包括文字、表情、特殊字符(如@xxx,链接)。
网上查找没找到合适的,要不只支持文字+表情,要不只支持文字+特殊字符,或者全是UILabel+UIImageVIew贴出来的(这个内存压力山大啊有木有),还有一种方案是加载HTML,这个可是需要强大的技术支撑,可惜我们这边不给力。
无奈之下只能自己写了个自定义的控件,是用Quartz 2D绘制的,写完后测试效果基本达到要求。先上个效果图,一会贴代码。写的不好请指正,轻拍。
PS:本来听说用core text效率更高,可惜我还没有研究过,准备过些时间专门研究一下。
时间比较长了,可能逻辑比较混乱, 一会把工程链接发了,可以直接下载源码看。
@protocol DDFTextViewDelegate.h头文件, 这个没什么可说的,就是一个点击事件的delegate,几个可设置属性,一个获取文本高度的静态方法。@optional- (void)ddfTextViewDidTouchSuccess:(NSString *)string;@end@interface DDFTextView : UIView@property (nonatomic, assign) id delegate;@property (nonatomic, copy) NSString *text;@property (nonatomic, retain) UIFont *font;@property (nonatomic, retain) UIColor *textColor;+ (float)heightOfText:(NSString *)text font:(UIFont *)font limitSize:(CGSize)limitSize;@end
然后是.m 文件中的几个成员变量
@interface DDFTextView () { NSMutableArray *_faceRanges; //表情索引 NSValue——NSRect NSMutableArray *_atRanges; //@字符索引 NSValue——NSRect UIColor *_atTextColor; //@颜色 BOOL _isTouching; //是否在触摸 NSMutableArray *_atRects; //@字符坐标 NSMutableArray---NSValue--CGRect int _atRectIndex; //当前成功触摸的索引值 BOOL _needAddRects;}@end然后是初始化方法
#pragma mark - init & preset- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self perset]; } return self;}- (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self perset]; } return self;}- (id)init { self = [super init]; if (self) { [self perset]; } return self;}- (void)perset { _faceRanges = [[NSMutableArray alloc] init]; _atRanges = [[NSMutableArray alloc] init]; _atRects = [[NSMutableArray alloc] init]; _isTouching = NO; self.textColor = [UIColor blackColor]; self.font = [UIFont systemFontOfSize:13]; self.backgroundColor = [UIColor clearColor]; _atTextColor = [[UIColor blueColor] retain];}这些初始化方法可以保证无论是代码创建还是xib创建都能够良好运行。
然后开始分析显示的文本信息
- (void)setText:(NSString *)text { if (_text != text) { [_text release]; _text = [text copy]; [self getFaceCheckedRanges]; [self getAtCheckedRanges]; if (_atRects.count) { [_atRects removeAllObjects]; } _needAddRects = YES; [self setNeedsDisplay]; }}这里面两个方法
[self getFaceCheckedRanges];[self getAtCheckedRanges];是用来根据正则表达式分析出表情和特殊文本的range信息并保存(此代码中特殊文本只添加了@xxx,如需要其他文本扩展相应的正则表达式即可)
- (void)getFaceCheckedRanges { [_faceRanges removeAllObjects]; NSString *faceRegexString = @"\\[jk\\d\\d\\]"; NSRegularExpression *faceRegex = [NSRegularExpression regularExpressionWithPattern:faceRegexString options:NSRegularExpressionCaseInsensitive error:NULL]; if (faceRegex) { NSArray *array = [faceRegex matchesInString:_text options:0 range:NSMakeRange(0, _text.length)]; for (NSTextCheckingResult *result in array) { [_faceRanges addObject:[NSValue valueWithRange:result.range]]; } }}- (void)getAtCheckedRanges { [_atRanges removeAllObjects]; NSString *atRegexString = @"@[\\w\u4e00-\u9fa5]+"; NSRegularExpression *atRegex = [NSRegularExpression regularExpressionWithPattern:atRegexString options:NSRegularExpressionCaseInsensitive error:NULL]; if (atRegex) { NSArray *array = [atRegex matchesInString:_text options:0 range:NSMakeRange(0, _text.length)]; for (NSTextCheckingResult *result in array) { [_atRanges addObject:[NSValue valueWithRange:result.range]]; } }}如上,分析出对应的NSRange,然后存入数组。正则表达式我也不是太懂,不懂别问我 ,网上很多,自己去查吧。
下面说说一个比较重要的成员变量 NSMutableArray *_atRects;
这个变量存储的是@xxx字符在空间中的 rect 信息, 用来判断触摸事件并且在触摸式突出显示效果,就想点击网页中的链接一样。
在绘制时,根据当前绘制的文字索引值 i ,将rect信息添加到数组中
NSRange atRange = [_atRanges[atIndex] rangeValue];if (NSLocationInRange(i, atRange)) { [_atTextColor set]; //add rects if (_needAddRects) { [self addRect:rect index:atIndex]; } if (i == atRange.location+atRange.length-1) { if (atIndex < _atRanges.count-1) { atIndex++; } }}其中 变量 i 是当前绘制的文字的索引值,atIndex 是 这个特殊字符所属的位置在 _atRanges 中的索引值 , - ( void )addRect:( CGRect )rect index:( int )index 方法将 rect 信息添加到数组中保存,并且合并相邻的 rect 信息。
- (void)addRect:(CGRect)rect index:(int)index { if (index > (_atRects.count-1) || _atRects.count == 0) { //新的, 添加 NSValue *va = [NSValue valueWithCGRect:rect]; NSMutableArray *arr = [NSMutableArray arrayWithObject:va]; [_atRects addObject:arr];// NSLog(@"--new"); }else if (index >= 0) { //已有, 扩展 NSMutableArray *array = _atRects[index]; BOOL needNewLine = YES; for (int i = 0; i < array.count; i++) { CGRect perRect = [array[i] CGRectValue]; if (perRect.origin.y == rect.origin.y && rect.origin.x > perRect.origin.x) { CGRect curRect = CGRectMake(perRect.origin.x, perRect.origin.y, perRect.size.width+rect.size.width, perRect.size.height); array[i] = [NSValue valueWithCGRect:curRect];// NSLog(@"++add"); needNewLine = NO; } } if (needNewLine){ //需要添加新的行 [array addObject:[NSValue valueWithCGRect:rect]];// NSLog(@"new line"); } }}好了, 准备工作完成, 准备绘制
- (void)drawRect:(CGRect)rect { //draw image & text CGPoint drawPoint = CGPointZero; int lenght = _text.length; float cHeight = [@" " sizeWithFont:_font].height; float width = self.frame.size.width; int faceIndex = 0; int atIndex = 0; for (int i = 0; i < lenght; i++) { @autoreleasepool { [_textColor set]; //image if (_faceRanges.count) { NSRange faceRange = [_faceRanges[faceIndex] rangeValue]; if (i == faceRange.location) { if (drawPoint.x + cHeight > width) { drawPoint.x = 0; drawPoint.y += cHeight; } NSString *name = [_text substringWithRange:NSMakeRange(faceRange.location+1, faceRange.length-2)]; UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"%@.png", name]]; [image drawInRect:CGRectMake(drawPoint.x, drawPoint.y, cHeight, cHeight)]; drawPoint.x += cHeight; i += faceRange.length-1; if (faceIndex < _faceRanges.count-1) { faceIndex++; } continue; } } //text NSString *aString = [_text substringWithRange:NSMakeRange(i, 1)]; CGSize size = [aString sizeWithFont:_font]; if (drawPoint.x + size.width > width) { drawPoint.x = 0; drawPoint.y += cHeight; } CGRect rect = CGRectMake(drawPoint.x, drawPoint.y, size.width, size.height); //@text if (_atRanges.count) { NSRange atRange = [_atRanges[atIndex] rangeValue]; if (NSLocationInRange(i, atRange)) { [_atTextColor set]; //add rects if (_needAddRects) { [self addRect:rect index:atIndex]; } if (i == atRange.location+atRange.length-1) { if (atIndex < _atRanges.count-1) { atIndex++; } } } } [aString drawInRect:rect withFont:_font]; drawPoint.x += size.width; } } //draw touch if (_isTouching) {// CGContextRef contex = UIGraphicsGetCurrentContext();// UIColor *fillColor = [UIColor colorWithWhite:0 alpha:.3];// NSArray *touchRects = _atRects[_atRectIndex];// for (NSValue *va in touchRects) {// CGRect rect = [va CGRectValue];// CGContextAddRect(contex, rect);// CGContextSetFillColorWithColor(contex, fillColor.CGColor);// }// CGContextDrawPath(contex, kCGPathFill); UIColor *fillColor = [UIColor colorWithWhite:0 alpha:.3]; [fillColor setFill]; NSArray *touchRects = _atRects[_atRectIndex]; for (NSValue *va in touchRects) { CGRect rect = [va CGRectValue]; UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:3]; [path fill]; } } _needAddRects = NO;}代码略长,全贴上了,这里面没什么高深的内容,就是逻辑稍稍复杂些。
其中 CGPoint drawPoint =CGPointZero; 是定位当前绘制的位置信息;int faceIndex =0; int atIndex = 0; 定位_faceRanges 和 _atRanges的索引值;
然后最后的这段代码用于点击特殊字符时绘制点击的效果, 就像在UIWebView中点击链接一样。
UIColor *fillColor = [UIColor colorWithWhite:0 alpha:.3];[fillColor setFill];NSArray *touchRects = _atRects[_atRectIndex];for (NSValue *va in touchRects) { CGRect rect = [va CGRectValue]; UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:3]; [path fill];}
_needAddRects 用来防止 _atRects 重复添加。
绘制完成,下面是判断触摸事件
开始
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (touches.count == 1) { UITouch *touch = [[event allTouches] anyObject]; CGPoint touchPoint = [touch locationInView:self]; for (NSMutableArray *arr in _atRects) { int index = [_atRects indexOfObject:arr]; for (NSValue *va in arr) { if (CGRectContainsPoint([va CGRectValue], touchPoint)) { _isTouching = YES; _atRectIndex = index; [self setNeedsDisplay]; // NSLog(@"%@", [_text substringWithRange:[_atRanges[index] rangeValue]]); return; } } } } [self.nextResponder touchesBegan:touches withEvent:event];}算法就是遍历 _atRects 中已存储的特殊字符的位置信息, 看看当前触摸点是否在内。
[self.nextResponder touchesBegan:touches withEvent:event];上面这行代码用于在触摸事件不成功时,即没有点击到特殊字符时, 将触摸事件向下传递,不要阻塞触摸的响应链。(想想这种情况:你把这个控件添加到一个UITableViewCell中, 并且几乎占据了cell的全部位置,而且还需要table响应didSelectedRowAtIndexPath, 那么如果没有把触摸事件向下传递,你就悲剧了 )。
然后是点击完成
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (_isTouching) { NSString *touchString = [_text substringWithRange:[_atRanges[_atRectIndex] rangeValue]]; NSString *returnString = [touchString stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:@""]; if ([self.delegate respondsToSelector:@selector(ddfTextViewDidTouchSuccess:)]) { [self.delegate ddfTextViewDidTouchSuccess:returnString]; } }else { [self.nextResponder touchesEnded:touches withEvent:event]; } _isTouching = NO; [self setNeedsDisplay];}- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { _isTouching = NO; [self setNeedsDisplay]; [self.nextResponder touchesCancelled:touches withEvent:event];}
当touchesEnded时,若点击特殊字符成功,即_isTouching为真时,响应delegate,并且将点击的字符传递出去。
然后别忘了添加一个重要的方法
- (void)layoutSubviews { [super layoutSubviews]; [self setNeedsDisplay];}当控件大小变化时,重新绘制。这个主要还是防止在cell中显示混乱。
最后添加上两个属性设置方法
- (void)setFont:(UIFont *)font { if (_font != font) { [_font release]; _font = [font retain]; if (_text.length) { [self setNeedsDisplay]; } }}- (void)setTextColor:(UIColor *)textColor { if (_textColor != textColor) { [_textColor release]; _textColor = [textColor retain]; if (_text.length) { [self setNeedsDisplay]; } }}大功告成。
,差点把他丢了,因为是自定义绘制,不能再用 NSString的 sizeWithFont: 来获取文本的高度,这就需要自己写一个了
+ (float)heightOfText:(NSString *)text font:(UIFont *)font limitSize:(CGSize)limitSize这个方法, 具体代码就不贴了。 后面吧工程发上来, 里面有。
工程放到 GitHub 上了, 需要的自己去下吧。
链接: