iOS 用UIView 封装一个简单的富文本展示 CoreTextView 类

废话开篇:本文简单讲述一下利用 UIView 如何封装一个简单的富文本展示 CoreTextView 的原理。无论是图片还是文字都是通过绘制而成的,那么,了解文本的绘制过程及如何对文本内容插入图片及实现部分文字区域实现点击事件就是本文要了解的内容。

先看一下简单的 demo 实现效果

屏幕录制2021-11-02 上午10.37.19.gif

实现的内容就是在文本中插入图片,并且圈定某一文本区域的点击事件。

步骤一、创建自定义 WSLCoreTextView ,实现文本的绘制

将文本绘制的任务放在 UIView 里面的 - (void)drawRect:(CGRect)rect 方法里。

代码如下

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    //y坐标轴反转
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    
    //文字描述
    NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"实现点击高亮的关键就是得到点击的文字区域的point和设置点击高亮的文字的rect是否在同一块区域内。"];
    [attributeStr addAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:22.f],NSForegroundColorAttributeName:[UIColor redColor]} range:NSMakeRange(0, attributeStr.length)];
    
    //绘制范围
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    NSInteger length = attributeStr.length;
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);
    
    //绘制
    CTFrameDraw(frame, context);
    
    //销毁
    CFRelease(frame);
    CFRelease(path);
    CFRelease(frameSetter);

}
复制代码

效果如下

image.png

步骤二、在特定区域内插入图片

上面的操作后,在当前 View 内已经绘制出了文本内容。

扫描二维码关注公众号,回复: 13790331 查看本文章

想要在文本的特定区域内绘制出一张图,那么,首先需要了解一下关于文本高度的组成部分。

image.png

Q 为例,首先,看到 baseline ,这个是基线,默认的文字垂直方向的对其方式是以基线为基准的,基线上面的高度为 ascent,基线下面的高度为 descent

[UIFont systemFontOfSize:29].leading;
复制代码

UIFont 有个 leading 属性,ascent + descent + leading = lineheight,行高的计算方式。因为,这里是要绘制一张图片,所以,只要设置一下 ascentdescent 值。

那么,就需要找到如何设置这两个参数的方法。

在次,需要了解一下文本的组成部分,绘制完成的文本可以获得某个区域内的文本行数对象集合 CFArrayRef ,这个集合里面存储这 CTLineRef 的信息,而每个 CTLineRef 又是由多个 CTRunRef 组成,CTRunRef 就是基本的绘制单元,并且这些对象所绑定的位置信息在文本绘制完成之后都是一样确定的了,通过计算是可以获得。所以,这里要做的就是找到需要替换图片的文本区域 CTRunRef 进行图片的渲染。

代码如下


    //图片占位符代理回调
    CTRunDelegateCallbacks callBacks;
    //在这个指针指向的地址下 CTRunDelegateCallbacks 大小的区域进行赋值 0 的操作
    memset(&callBacks, 0, sizeof(CTRunDelegateCallbacks));

    callBacks.version = kCTRunDelegateCurrentVersion;
    
    //ascentCallBacks 是一个 c 语言函数,这里返回的就是上面说的 ascent 的数值
    callBacks.getAscent = ascentCallBacks;
    //descentCallBacks 是一个 c 语言函数,这里返回的就是上面说的 descent 的数值
    callBacks.getDescent = descentCallBacks;
    //widthCallBacks 是一个 c 语言函数,这里返回的是图片的宽度
    callBacks.getWidth = widthCallBacks;
    //设置图片的宽度
    NSDictionary * info = @{@"h":@(80),@"w":@(80)};

    //占位代理符,将代理回调绑定在代理对象上
    CTRunDelegateRef delegate = CTRunDelegateCreate(&callBacks,( __bridge void *)info);
    //创建一个空字符,插入的特定位置,目的是将这个空字符串进行图片的替换
    unichar placeHolder = 0xFFFC;//空字符串
    NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
    NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];

    //将代理绑定在新建的占位符文本上
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
    //释放c对象
    CFRelease(delegate);
    //在文本的第3个位置插入图片
    [attributeStr insertAttributedString:placeHolderAttrStr atIndex:2];
复制代码

绘制代理回调 c 方法,方法参数 ref,就是绑定在绘制代理对象上的参数值,这里就是 dic

//获取 ascent
static CGFloat ascentCallBacks(void * ref)
{
    return [(NSNumber *)[( __bridge NSDictionary *)ref valueForKey:@"h"] floatValue] / 2.0;

}

//获取 descent
static CGFloat descentCallBacks(void *ref)
{
    return [(NSNumber *)[( __bridge NSDictionary *)ref valueForKey:@"h"] floatValue] / 2.0;;

}

//获取 width
static CGFloat widthCallBacks(void *ref)
{
    return [(NSNumber *)[( __bridge NSDictionary *)ref valueForKey:@"w"] floatValue];

}
复制代码

好了,通过上面的代码,在原来的字符串上就在指定位置添加了一个绑定了绘制代理对象 的字符串,当然,这些操作是在指向文本绘制之前进行的。

绘制完成后进行图片的插入,

代码如下


    //绘制图片
    
    //这里获取当前渲染范围内的 CTLineRef 集合 CFArrayRef
    CFArrayRef lines = CTFrameGetLines(frame);
    //获取 CTLineRef 个数
    CFIndex lineCount = CFArrayGetCount(lines);
    //保存每一行的坐标点,这里保存是用来对图片绘制进行坐标定位
    CGPoint origins[lineCount];

    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);

    for (int i = 0; i < lineCount; i++) {
        //获取 CTLineRef 信息
        CTLineRef lineRef = CFArrayGetValueAtIndex(lines, i);
        //获取 CTRunRef 集合
        CFArrayRef runs = CTLineGetGlyphRuns(lineRef);

        CFIndex runCount = CFArrayGetCount(runs);

        for (int j = 0; j < runCount; j++) {
            //获取 CTRunRef,这里就是代表的是每一个独立的字符串
            CTRunRef runRef = CFArrayGetValueAtIndex(runs, j);
            //获取 CTRunRef 绑定的特性
            NSDictionary * attributes = (id)CTRunGetAttributes(runRef);
            //特性无,直接略过,特性里绑定了对文字绘制的描述及之前绑定绘制代理的对象
            if (!attributes) {

                continue;

            }

            //绘制图片,找到绘制代理对象
            CTRunDelegateRef delegateRef = ( __bridge CTRunDelegateRef)[attributes valueForKey:(void *)kCTRunDelegateAttributeName];
            if (delegateRef) {
                //获取绑定的参数数值,这里指的就是存有 宽、高 的 dic
                id info = (id)CTRunDelegateGetRefCon(delegateRef);

                if ([info isKindOfClass:[NSDictionary class]]) {
                    //获取之前存储好的每行的起点(x,y)
                    CGPoint origin = origins[i];
                    //获取当前字符串对应的 x 起点值,通过上述两步,就可以获取到特定字符的 point
                    CGFloat offSetX = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
                    //获取宽
                    CGFloat w = [info[@"w"] floatValue];
                    //获取高
                    CGFloat h = [info[@"h"] floatValue];
                    //在计算好的位置区域内绘制图片
                    CGContextDrawImage(context, CGRectMake(origin.x + offSetX, origin.y - 40, w, h), [UIImage imageNamed:@"shuaxin"].CGImage);

                }

            }

        }

    }
复制代码

效果如下

image.png

调整 ascent 、 descent 可以调整文字跟图片的对其方式,其实本质还是通过修改坐标点在绘制。

如图

image.png

步骤三、实现特定区域文本点击高亮响应事件

首先,创建一个高亮存储类,它里面保存需要点击高亮的文字 range ,点击后文字颜色 backgroundColor,通过计算得到的所有可点击进行高亮操作的实际 rect 区域。

@interface HightlightAction : NSObject

@property (nonatomic,assign) NSRange range;

@property (nonatomic,strong) UIColor * backgroundColor;

@property (nonatomic,strong) NSMutableArray * hightlightRects;

@end
复制代码

实现思路:

给当前的 view 添加 touchbegintouchesEndedtouchesCancelled 方法,记录当前点击的坐标点,然后进行比对,当前的坐标点是否在 HightlightAction 对象存储的所有可高亮点击的 rect 范围内,如果条件成立,重新绘制 UI,然后在绘制的时候判断指定的文字区域是否是存在高亮状态,是的话就将 HightlightAction 对象存储 backgroundColor 值给当前区域的文字。当手势取消或者结束后,再次重绘 UI,恢复原始状态。

代码如下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

{
    UITouch * touch = touches.anyObject;
    CGPoint p = [touch locationInView:touch.view];
    //遍历检测触点是否在高亮显示的需求区域内
    for (int i = 0; i < self.action.hightlightRects.count; i++) {

        CGRect rect = [self.action.hightlightRects[i] CGRectValue];
        //是否高亮赋值
        self.isHeightLight = CGRectContainsPoint(rect, p);

        if (self.isHeightLight) {

            break;

        }

    }

    //NSLog(@"t.y = %.2f,p.y = %.2f",self.action.hightlightRect.origin.y,p.y);
    //重绘
    [self setNeedsDisplay];

}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

{
    //取消高亮
    self.isHeightLight = NO;
    //重绘
    [self setNeedsDisplay];

}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

{
    //取消高亮
    self.isHeightLight = NO;
    //重绘
    [self setNeedsDisplay];

}
复制代码

现在,就需要确定需要点击高亮显示的文字实际的坐标信息了,还是在文本绘制完成后,通过遍历里面的 CTRunRef 来获取每一个符合高亮状态字符串,保存对应可高亮文本的实际坐标 rect

这里先声明并赋值一个 指针常量,目的是在后面为字符串绑定属性 value 的时候 key 的值是统一的。

static NSString * const kHighlightAttributeName = @"wslKHighlightAttributeName";
复制代码

这里简单说一下 常量指针指针常量

image.png

kHighlightAttributeName 指针常量 其实是限定了 = 右侧的对象地址。

创建一个 HightlightAction 高亮对象,代码如下:

    //高亮
    HightlightAction * action = [HightlightAction new];
    //定义高亮颜色
    action.backgroundColor = [UIColor greenColor];
    //定一个高亮区域
    action.range = NSMakeRange(3, 3);
    //这里绑定需要高亮的文字区域
    [attributeStr addAttribute:kHighlightAttributeName value:action range:action.range];
    //添加了下划线属性
    [attributeStr addAttribute:NSUnderlineStyleAttributeName value:@1 range:action.range];
    //根据状态值,来判断是否高亮,来修改文本颜色显示
    if (self.isHeightLight) {

        [attributeStr addAttribute:NSForegroundColorAttributeName value:action.backgroundColor range:action.range];

    }
复制代码

在遍历 CTRunRef 里进行可高亮文本区域的实际 rect 的获取及保存。

代码如下:

   //获取设置高亮参数
   HightlightAction * attrHightlightAction = [attributes valueForKey:kHighlightAttributeName];
   
   if (attrHightlightAction) {
        //获取 line 的起点坐标
        CGPoint normalOrigin = origins[i];
        //当前文本在当前行的 x 偏移
        CGFloat normalOffSetX = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
        
        CGFloat normalAscent;//上行高

        CGFloat normalDescent;//下行高

        CGFloat normalLeading;//行间距
        //获取 Ascent Descent Leading 的实际值,用来计算文字的覆盖区域
        CTLineGetTypographicBounds(lineRef, &normalAscent, &normalDescent, &normalLeading);
        //计算行高
        CGFloat lineHeight = normalAscent + ABS(normalDescent) + normalLeading;
        //计算宽度
        CGFloat normalWidth = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), NULL, NULL, NULL);
        //计算文本在当前 view 上的坐标 rect
        CGRect hightlightRect = CGRectMake(normalOrigin.x + normalOffSetX,rect.size.height - normalOrigin.y - lineHeight, normalWidth, lineHeight);
        //保存位置信息,因为需要高亮的是一段文字,所以,这里会走多次
        [attrHightlightAction.hightlightRects addObject:@(hightlightRect)];
        //临时保存
         self.action = attrHightlightAction;

   }
复制代码

好了,需要高亮的文字实际区域获取就完成了,那么,与之前的 touchbegin 、 touchesEnded 及 touchesCancelled 方法进行结合重绘就实现了简单的文字点击高亮效果。

到这,简单的富文本展示 CoreTextView 类就完成了,其本质还是在不断的获取位置、计算位置、添加展示的过程。代码拙劣,大神勿笑。

猜你喜欢

转载自juejin.im/post/7025860567550132238