Quartz 2D 自定义富文本控件

本文介绍了一个自定义的富文本控件,能够支持文字、表情、特殊字符(如@xxx)的混合显示,并通过Quartz2D进行绘制。该控件支持表情和特殊字符的范围分析,实现点击效果,适用于需要展示复杂文本内容的应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

点击下载



内容概要:该研究通过在黑龙江省某示范村进行24小时实地测试,比较了燃煤炉具与自动/手动进料生物质炉具的污染物排放特征。结果显示,生物质炉具相比燃煤炉具显著降低了PM2.5、CO和SO2的排放(自动进料分别降低41.2%、54.3%、40.0%;手动进料降低35.3%、22.1%、20.0%),但NOx排放未降低甚至有所增加。研究还发现,经济性和便利性是影响生物质炉具推广的重要因素。该研究不仅提供了实际排放数据支持,还通过Python代码详细复现了排放特征比较、减排效果计算和结果可视化,进一步探讨了燃料性质、动态排放特征、碳平衡计算以及政策建议。 适合人群:从事环境科学研究的学者、政府环保部门工作人员、能源政策制定者、关注农村能源转型的社会人士。 使用场景及目标:①评估生物质炉具在农村地区的推广潜力;②为政策制定者提供科学依据,优化补贴政策;③帮助研究人员深入了解生物质炉具的排放特征和技术改进方向;④为企业研发更高效的生物质炉具提供参考。 其他说明:该研究通过大量数据分析和模拟,揭示了生物质炉具在实际应用中的优点和挑战,特别是NOx排放增加的问题。研究还提出了多项具体的技术改进方向和政策建议,如优化进料方式、提高热效率、建设本地颗粒厂等,为生物质炉具的广泛推广提供了可行路径。此外,研究还开发了一个智能政策建议生成系统,可以根据不同地区的特征定制化生成政策建议,为农村能源转型提供了有力支持。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值