博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Quartz 2D 自定义富文本控件
阅读量:4284 次
发布时间:2019-05-27

本文共 12273 字,大约阅读时间需要 40 分钟。

之前项目需要展示富文本,包括文字、表情、特殊字符(如@xxx,链接)。

网上查找没找到合适的,要不只支持文字+表情,要不只支持文字+特殊字符,或者全是UILabel+UIImageVIew贴出来的(这个内存压力山大啊有木有),还有一种方案是加载HTML,这个可是需要强大的技术支撑,可惜我们这边不给力。

无奈之下只能自己写了个自定义的控件,是用Quartz 2D绘制的,写完后测试效果基本达到要求。先上个效果图,一会贴代码。写的不好请指正,轻拍大笑

PS:本来听说用core text效率更高,可惜我还没有研究过,准备过些时间专门研究一下。

时间比较长了,可能逻辑比较混乱, 一会把工程链接发了,可以直接下载源码看。

@protocol DDFTextViewDelegate 
@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
.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 上了, 需要的自己去下吧。

链接: 

你可能感兴趣的文章
MTD bad Block issue
查看>>
How to change network interface name
查看>>
ubifs and ubi and mtd
查看>>
shell script set 用法
查看>>
英文序數寫法與唸法 Ordinal Numbers(轉載)
查看>>
DVB-S info
查看>>
绿盟扫描操作指导
查看>>
理解链路本地址与站点本地地址
查看>>
/proc/mtd 各个参数含义 -- linux内核
查看>>
linux nand flash常用命令
查看>>
NESSUS扫描操作指导
查看>>
C语言读取文件大小,载入文件全部内容
查看>>
C语言 static静态变量的作用
查看>>
Linux(C/C++)下的文件操作open、fopen与freopen
查看>>
C语言 文件操作的头文件
查看>>
C语言的常用库函数(dos)之四(dir.h文件下的一些函数)
查看>>
warning: jobserver unavailable: using -j1. Add `+' to parent make rule问题怎么解决
查看>>
防火墙报文转发流程
查看>>
以太坊创始人:17岁的亿万富翁养成记
查看>>
linux下IPTABLES配置详解
查看>>