Quartz 2D 自定义富文本控件

之前项目需要展示富文本,包括文字、表情、特殊字符(如@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

点击下载



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值