之前项目需要展示富文本,包括文字、表情、特殊字符(如@xxx,链接)。
网上查找没找到合适的,要不只支持文字+表情,要不只支持文字+特殊字符,或者全是UILabel+UIImageVIew贴出来的(这个内存压力山大啊有木有),还有一种方案是加载HTML,这个可是需要强大的技术支撑,可惜我们这边不给力。
无奈之下只能自己写了个自定义的控件,是用Quartz 2D绘制的,写完后测试效果基本达到要求。先上个效果图,一会贴代码。写的不好请指正,轻拍。
PS:本来听说用core text效率更高,可惜我还没有研究过,准备过些时间专门研究一下。
时间比较长了,可能逻辑比较混乱, 一会把工程链接发了,可以直接下载源码看。
@protocol DDFTextViewDelegate <NSObject>
@optional
- (void)ddfTextViewDidTouchSuccess:(NSString *)string;
@end
@interface DDFTextView : UIView
@property (nonatomic, assign) id<DDFTextViewDelegate> 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
.h头文件, 这个没什么可说的,就是一个点击事件的delegate,几个可设置属性,一个获取文本高度的静态方法。
然后是.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 上了, 需要的自己去下吧。
链接: https://github.com/DefuDong/DDFTextView
点击下载