Da xia 的技术博客

不积跬步,无以至千里.

北京


欢迎访问我的博客,这里主要记录我的工作和生活

CoreText 实现镂空文字效果

CoreText 基础

CoreText 是用于处理文字和字体的底层技术。它直接和 Core Graphics 交互,具有很高的文字渲染效率。CoreText 对象的架构图

CTFramesetter 接受 NSSAttributedString 和 CGPath 参数,创建一个或者多个 CTFrame。每个 CTFrame 代表一个段落,包含一行或者多行文字,也就是 CTLine。在创建 CTFrame 时有几个生成了 CTLine,它可以直接渲染到 graphics context。CTRun 是一组具有共同属性和方向的连续字形(glyph),每行可以报考一个或者多个 CTRun。

绘制一段文字

使用 CoreText 绘制一段文字一般需要估下过程: * 创建 AttributedString,定义段落、字体等样式 * 获取 context,有时还需要转换 context 的坐标系 * 创建 CTFramesetter、绘制区域的 Path,生成 CTFrame * 绘制,整段绘制(CTFrameDraw)或者逐行绘制(CTLineDraw)

下面的代码逐行绘制了一段文字,并为每一行画了一个红色边框:

 // UIView 子类中
- (void)drawRect:(CGRect)rect
{
    // 1. 获取 context
    CGContextRef context = UIGraphicsGetCurrentContext();

    // 2. 翻转坐标系
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

    // 3. CTFramesetterRef
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self.attrString);

    // 4. 创建绘制区域
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);

    // 5. 创建 CTFrame
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, self.attrString.length), path, NULL);


    // 6. 绘制
    //CTFrameDraw(frame, context);

    // 逐行绘制
    CFArrayRef Lines = CTFrameGetLines(frame);
    CFIndex lineCount = CFArrayGetCount(Lines);
    CGPoint origins[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
    for (CFIndex i = 0 ; i < lineCount; i ++) {
        CTLineRef line = CFArrayGetValueAtIndex(Lines, i);

        CGPoint lineOrigin = origins[i];
        CGContextSetTextPosition(context, lineOrigin.x, lineOrigin.y);

        CGRect lineBounds = CTLineGetImageBounds((CTLineRef)line, context);
        NSLog(@"lineBounds = %@",NSStringFromCGRect(lineBounds));

        CGContextSetLineWidth(context, 1.0);
        CGContextAddRect(context, lineBounds);
        CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
        CGContextStrokeRect(context, lineBounds);
        CTLineDraw(line, context);
    }

    CFRelease(frame);
    CFRelease(path);
    CFRelease(frameSetter);
}

- (void)setAttrString:(NSAttributedString *)attrString
{
    if (_attrString != attrString) {
        _attrString = attrString;
        [self setNeedsDisplay];
    }
}

绘制的文字:

上面代码中翻转了坐标系,这是因为 drawRect: 方法中获取的当前 contenx 已经被调整为 UIKit 的坐标系了(左上角是原点),但是 CoreText 使用的是 Core Graphics 的坐标系(左下角是原点),所以要把当前 context 坐标系还原成左下角为原点。

镂空文字效果

实现镂空文字最简单的方法就是请设计师给一个文字镂空的切图,如果文案是动态配置的,这种方法就不行了。使用代码实现镂空效果要用到 CALayer 的 mask 属性。mask 也是一个 CALayer 对象,但只是用到它的轮廓,在轮廓内的父图层才能显示出来。

为了实现文字镂空效果,我们可以借助 CoreText 取出文字的 Path,创建 CAShapeLayer 绘制出文字外部的轮廓,将 CAShapeLayer 作为宿主图层的 mask。

第一步,取出文字的 path。

+ (UIBezierPath *)singleLinePathWithText:(NSString *)text font:(UIFont *)font
{
    CTFontRef ctFont = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
    if (!ctFont) {
        return nil;
    }
    // 创建 attributedString
    NSDictionary *attrs = @{(__bridge id)kCTFontAttributeName : (__bridge id)ctFont};
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:text attributes:attrs];
    CFRelease(ctFont);

    // 创建 Line
    CTLineRef line = CTLineCreateWithAttributedString((__bridge CFTypeRef)attrString);
    if (!line) {
        return nil;
    }

    CGMutablePathRef cgPath = CGPathCreateMutable();
    CFArrayRef runs = CTLineGetGlyphRuns(line);
    for (CFIndex iRun = 0, iRunMax = CFArrayGetCount(runs); iRun < iRunMax; iRun ++) {
        CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, iRun);
        CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);
        for (CFIndex iGlyph = 0, iGlyphMax = CTRunGetGlyphCount(run); iGlyph < iGlyphMax; iGlyph++) {
            CFRange glyphRange = CFRangeMake(iGlyph, 1);
            CGGlyph glyph;
            CGPoint position;
            CTRunGetGlyphs(run, glyphRange, &glyph);
            CTRunGetPositions(run, glyphRange, &position);
            // 读取文字的 path
            CGPathRef glyphPath = CTFontCreatePathForGlyph(runFont, glyph, NULL);
            if (glyphPath) {
                CGAffineTransform transform = CGAffineTransformMakeTranslation(position.x, position.y);
                CGPathAddPath(cgPath, &transform, glyphPath);
                CGPathRelease(glyphPath);
            }
        }
    }

    UIBezierPath *path = [UIBezierPath bezierPathWithCGPath:cgPath];
    CGRect boundingBox = CGPathGetPathBoundingBox(cgPath);
    CFRelease(cgPath);
    CFRelease(line);

    // 翻转坐标系(变成左上角为原点)
    [path applyTransform:CGAffineTransformMakeScale(1.0, -1.0)];
    [path applyTransform:CGAffineTransformMakeTranslation(0.0, boundingBox.size.height)];

    return path;
}

第二步,创建 masklayer。我封装了一个简单的 View,将文字置于与视图的中间。

// UIView 子类
@implementation ZDXCutoutLabel

- (void)updateWithText:(NSString *)text font:(UIFont *)font
{
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];
    UIBezierPath *textPath = [UIBezierPath singleLinePathWithText:text font:font];

    CGRect textRect = CGPathGetPathBoundingBox(textPath.CGPath);
    CGRect centerRect = RectCenteredInRect(textRect, self.bounds);
    CGAffineTransform tansform = CGAffineTransformMakeTranslation(centerRect.origin.x - textRect.origin.x, centerRect.origin.y - textRect.origin.y);
    [textPath applyTransform:tansform];
    [path appendPath:textPath];

    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = [path CGPath];
    self.layer.mask = pathLayer;
}

@end

镂空文字效果,背景是一个 gif 图片。

图层结构:

Demo 在这里

最近的文章

Porter-Duff Blending 实现镂空文字效果

Blend Modes在 Core Graphics 中,我们通过设置 Blend Mode 混合两张图片,或者把图片混合到当前 context 的背景中。通常的使用步骤如下:绘制背景设置 CGContextSetBlendMode绘制图片到背景上有两张图片使用 kCGBlendModeMultiply 混合后的结果是这样的:CGBlendMode 定义了很多种混合模式供我们使用,其中第一部分的混合效果在 Using Blend Modes with Images 可以查看。下一节一起来了...…

继续阅读
更早的文章

识别 Emoji 表情

在处理用户输入的内容时候,有些场景不希望输入 emoji,在 iOS 中如何检测 emoji?结合网上的一些资料,我总结一下识别 emoji 的方法,在介绍 emoji 之前先了解下字符集和字符编码相关的基础知识。Unicode 字符集Unicode 标准为世界上几乎所有的[1]书写系统里所使用的每一个字符或符号定义了一个唯一的数字。这个数字叫做码点(code points),以 U+xxxx 这样的格式写成,格式里的 xxxx 代表四到六个十六进制的数。最初,Unicode 编码是被设计...…

继续阅读