CoreText 原理

作者: CoderLocus | 来源:发表于2016-06-28 11:05 被阅读798次

    CoreText

    原理

    为了方便使用,需要先创建一个自定义UIView,我们将在drawRect函数里使用CoreText。

    - (void)drawRect:(CGRect)rect {
        [super drawRect:rect];
    
        CGContextRef context = UIGraphicsGetCurrentContext();
    
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        CGContextTranslateCTM(context, 0, self.bounds.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
    
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddEllipseInRect(path, NULL, self.bounds);
    
        NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"Hello World"];
        CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
        CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attString.length), path, NULL);
    
        CTFrameDraw(frame, context);
    
        CFRelease(frame);
        CGPathRelease(path);
        CGContextRelease(context);
    }
    
    1. 我们首先创建了一个CGContextRef上下文。
    2. 因为CoreText的坐标系是以右下角为原点,所以我们将CoreText坐标系翻转一下。
    3. 创建了一个CGPath,来完成绘制文字的区域。CoreText支持矩形、环形。因为用整个View来做显示区域,所以我们通过self.bounds来创建CGPath
    4. 在CoreText我们要渲染的文字需要用NSAttributedString来创建,它允许你对文字颜色、字体、大小设置不同的样式。
    5. CTFramesetterRef是CoreText非常重要的一个类,他管理了你得字体和文本渲染块(CTFrame)。最简单的创建就是通过NSAttributedString来创建一个CTFramesetterRef。然后通过刚创建的CTFramesetterRef来创建一个CTFrameRefCTFramesetterCreateFrame的参数分别是frameSetter、将要显示文字的Range(这里☞长度attString.length)、文本要显示的区域(刚刚创建的path)和frameAttributes(可以为空)。
    6. 通过CTFrameDraw在给定的context里绘制frame
    7. 最后不要忘了清理资源。

    CoreText对象模型

    Unknown.jpg

    在CTFrame内部是由多个CTline组成,每行CTline又是由多个CTRun组成
    每个CTRun代表一组风格一致的文本(CTline和CTRun的创建不需要我们管理)
    在CTRun中我们可以设置代理来指定绘制此组文本的宽高和排列方式等信息

    我们通过NSAttributedString创建一个CTFramesetter,这时候会自动创建一个 CTTypesetter实例,它负责管理字体,下面通过CTFramesetter来创建一个或多个frame来渲染文字。然后Core Text会根据frame的大小自动创建CTLine(每行对应一个CTLine)和CTRun(相同格式的一个或多个相邻字符组成一个CTRun)。
    举例来说,Core Text将创建一个CTRun来绘制一些红色文字,然后创建一个CTRun来绘制纯文本,然后再创建一个CTRun来绘制加粗文字等等。要注意,你不需要自己创建CTRun,Core Text将根据NSAttributedString的属性来自动创建CTRun。每个CTRun对象对应不同的属性,正因此,你可以自由的控制字体、颜色、字间距等等信息。
    #import "CoreTextData.h"

    @interface CoreTextData : NSObject
    /** 文本绘制的区域大小 */
    @property (nonatomic, assign) CTFrameRef ctFrame;
    /** 文本绘制区域高度 */
    @property (nonatomic, assign) CGFloat height;
    /** 文本中存储图片信息数组 */
    @property (nonatomic, strong) NSMutableArray *imageArray;
    /** 文本中存储链接信息数组 */
    @property (nonatomic, strong) NSMutableArray *linkArray;
    @end
    @implementation CoreTextData
    
        - (void)setCtFrame:(CTFrameRef)ctFrame {
            if (_ctFrame != ctFrame) {
                if (_ctFrame != nil) {
                    CFRelease(_ctFrame);
                }
                CFRetain(ctFrame);
                _ctFrame = ctFrame;
            }
        }
    
    - (void)dealloc {
        if (_ctFrame != nil) {
            CFRelease(_ctFrame);
            _ctFrame = nil;
        }
    }
    
    - (void)setImageArray:(NSArray *)imageArray {
        _imageArray = imageArray;
        [self fillImagePosition];
    }
    
    - (void)fillImagePosition {
        if (self.imageArray.count == 0) {
            return;
        }
        
        // 此处利用CTRun代理设置一个空白的字符给定宽高,最后在利用CGContextDrawImage将其绘制
        
        // 获取CTFrame中所有的line
        NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
        NSUInteger lineCount = [lines count];
        // 利用CGPoint数组获取所有line的起始坐标
        CGPoint lineOrigins[lineCount];
        CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
    
        // 获取图片数组中第一个图片信息
        int imgIndex = 0;
        CoreTextImageData * imageData = self.imageArray[0];
    
        for (int i = 0; i < lineCount; ++i) {
            // 如果不存在图片则返回
            if (imageData == nil) {
                break;
            }
            // 存在图片信息则获取图片具体位置信息
            // 获取每行信息
            CTLineRef line = (__bridge CTLineRef)lines[i];
            // 得到每行的CTRun信息,并遍历
            NSArray * runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
            for (id runObj in runObjArray) {
            CTRunRef run = (__bridge CTRunRef)runObj;
                NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
                // 获取CTRun的代理信息,若无代理信息则直接进入下次循环
                CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
                if (delegate == nil) {
                    continue;
                }
            
            // 若有代理信息,判断代理信息是否为字典,不是直接进入下次循环
            NSDictionary * metaDic = CTRunDelegateGetRefCon(delegate);
            if (![metaDic isKindOfClass:[NSDictionary class]]) {
                continue;
            }
            
            CGRect runBounds;
            CGFloat ascent;
            CGFloat descent;
            // 找到CTRunDelegate中的宽度并给上升和下降高度赋值
            runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            runBounds.size.height = ascent + descent;
            
            // 获取CTRun在x上的偏移量
            CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            // 起点坐标
            runBounds.origin.x = lineOrigins[i].x + xOffset;
            runBounds.origin.y = lineOrigins[i].y;
            runBounds.origin.y -= descent;
            
            // 获取路径,并利用路径获取绘制视图的Rect
            CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
            CGRect colRect = CGPathGetBoundingBox(pathRef);
            
            CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
            
            // 保存图片位置信息
            imageData.imagePosition = delegateBounds;
            imgIndex++;
            if (imgIndex == self.imageArray.count) {
                imageData = nil;
                break;
            } else {
                   imageData = self.imageArray[imgIndex];
            }
          }
       }
    }
    @end
    
    #import "CoreTextImageData.h"
    @interface CoreTextImageData : NSObject
    
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, assign) CGRect imagePosition;
    @end
    
    
    
    
    #import "CoreTextLinkData.h"
    
    @implementation CoreTextLinkData
    + (CoreTextLinkData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(CoreTextData *)data {
    
        CTFrameRef ctFrame = data.ctFrame;
        CFArrayRef lines = CTFrameGetLines(ctFrame);
        if (lines == nil) {
            return nil;
        }
    
        CFIndex linesCount = CFArrayGetCount(lines);
        CoreTextLinkData *linkdata = nil;
    
        CGPoint linesOrigins[linesCount];
        CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), linesOrigins);
    
        //由于CoreText和UIKit坐标系不同所以要做个对应转换
        CGAffineTransform transform = CGAffineTransformMakeTranslation(0, view.bounds.size.height);
        transform = CGAffineTransformScale(transform, 1, -1);
    
        for (int i = 0; i < linesCount; i ++) {
            CGPoint linePoint = linesOrigins[i];
            CTLineRef line = CFArrayGetValueAtIndex(lines, i);
            //获取当前行的rect信息
            CGRect flippedRect = [self getLineBounds:line point:linePoint];
            //将CoreText坐标转换为UIKit坐标
            CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);
            //判断点是否在Rect当中
            if (CGRectContainsPoint(rect, point)) {
                //获取点在line行中的位置
                CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect));
                //获取点中字符在line中的位置(在属性文字中是第几个字)
                CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
                //判断此字符是否在链接属性文字当中
                linkdata = [self linkAtIndex:idx linkArray:data.linkArray];
                break;
            }
        }
        return linkdata;
    }
    
    + (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point {
        //配置line行的位置信息
        CGFloat ascent = 0;
        CGFloat descent = 0;
        CGFloat leading = 0;
        //在获取line行的宽度信息的同时得到其他信息
        CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGFloat height = ascent + descent;
        return CGRectMake(point.x, point.y, width, height);
    }
    
    + (CoreTextLinkData *)linkAtIndex:(CFIndex)i linkArray:(NSArray *)linkArray {
        CoreTextLinkData *linkdata = nil;
        for (CoreTextLinkData *data in linkArray) {
            if (NSLocationInRange(i, data.range)) {
                linkdata = data;
                break;
            }
        }
        return linkdata;
    }
    @end

    相关文章

      网友评论

        本文标题:CoreText 原理

        本文链接:https://www.haomeiwen.com/subject/lbltjttx.html