MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的Core Text高级文本排版

2022-11-201.3k 阅读

Core Text 基础概念

在深入探讨 Objective-C 中 Core Text 的高级文本排版之前,我们先来了解一些 Core Text 的基础概念。Core Text 是 iOS 和 macOS 平台上一个强大的底层文本处理框架,它提供了精确控制文本布局、字体、样式等方面的能力。

CTFramesetter

CTFramesetter 是 Core Text 中用于创建文本框架的核心对象。它根据给定的文本字符串、字体信息以及其他排版属性,生成一个用于后续布局的框架设置对象。例如,我们可以通过以下代码创建一个 CTFramesetter:

CFStringRef string = CFSTR("Hello, Core Text!");
CFAttributedStringRef attributedString = CFAttributedStringCreate(kCFAllocatorDefault, string, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedString);
CFRelease(attributedString);

在上述代码中,首先创建了一个简单的字符串 Hello, Core Text!,然后将其转换为 CFAttributedStringRef 类型,最后使用这个属性字符串创建了 CTFramesetterRef 对象。

CTFrame

CTFrame 是基于 CTFramesetter 生成的实际文本布局框架。它定义了文本在特定区域内的排版方式,包括换行、分页等行为。一旦有了 CTFramesetter,就可以通过以下方式创建 CTFrame:

CGRect rect = CGRectMake(0, 0, 200, 200);
CGPathRef path = CGPathCreateWithRect(rect, NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CGPathRelease(path);
CFRelease(framesetter);

这里我们定义了一个矩形区域 rect,并创建了一个与之对应的 CGPathRef。然后使用 CTFramesetter 在这个路径上创建了 CTFrame。

高级文本排版之字体设置

在 Core Text 中,字体的设置对于文本的显示效果至关重要。除了基本的字体名称和大小设置外,还可以进行许多高级的字体相关操作。

自定义字体

有时候系统提供的字体无法满足需求,这时就需要使用自定义字体。首先要将字体文件添加到项目中,然后在代码中加载并使用。例如,假设我们有一个名为 MyCustomFont.ttf 的字体文件:

// 加载字体
NSURL *fontURL = [[NSBundle mainBundle] URLForResource:@"MyCustomFont" withExtension:@"ttf"];
CFErrorRef error = NULL;
CTFontDescriptorRef fontDescriptor = CTFontDescriptorCreateWithURL((__bridge CFURLRef)fontURL);
CTFontRef customFont = CTFontCreateWithFontDescriptor(fontDescriptor, 16.0, NULL);
if (customFont == NULL) {
    NSLog(@"Failed to load custom font: %@", (__bridge NSError *)error);
    CFRelease(fontDescriptor);
    return;
}
CFRelease(fontDescriptor);

// 使用自定义字体设置属性字符串
CFMutableAttributedStringRef attributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFStringRef string = CFSTR("Using custom font");
CFAttributedStringReplaceString(attributedString, CFRangeMake(0, 0), string);
CFAttributedStringSetAttribute(attributedString, CFRangeMake(0, CFStringGetLength(string)), kCTFontAttributeName, customFont);
CFRelease(customFont);

上述代码首先从项目资源中获取字体文件的 URL,然后创建字体描述符并基于此创建字体对象。最后将这个自定义字体应用到属性字符串上。

字体样式调整

除了使用不同字体,还可以对字体的样式进行调整,如加粗、倾斜等。在 Core Text 中,可以通过设置 kCTFontTraitsAttributeName 来实现。

CFMutableDictionaryRef fontTraits = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFNumberRef boldTrait = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &kCTFontBoldTrait);
CFDictionarySetValue(fontTraits, kCTFontTraitName, boldTrait);
CFRelease(boldTrait);

CTFontRef originalFont = CTFontCreateWithName(CFSTR("Helvetica"), 16.0, NULL);
CTFontRef boldFont = CTFontCreateCopyWithTraits(originalFont, 0, NULL, fontTraits, NULL);
CFRelease(originalFont);
CFRelease(fontTraits);

CFMutableAttributedStringRef attributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFStringRef string = CFSTR("Bold text");
CFAttributedStringReplaceString(attributedString, CFRangeMake(0, 0), string);
CFAttributedStringSetAttribute(attributedString, CFRangeMake(0, CFStringGetLength(string)), kCTFontAttributeName, boldFont);
CFRelease(boldFont);

这里首先创建了一个包含加粗特征的字典,然后基于原始字体创建了加粗字体,并应用到属性字符串上。

段落排版

段落排版是 Core Text 高级文本排版的重要部分,它控制着文本的缩进、行距、对齐方式等。

缩进设置

缩进可以通过设置 kCTParagraphStyleAttributeName 中的 firstLineHeadIndentheadIndent 属性来实现。

CTParagraphStyleSetting settings[] = {
    {kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &(CGFloat){20.0}},
    {kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), &(CGFloat){10.0}}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));

CFMutableAttributedStringRef attributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFStringRef string = CFSTR("This is a paragraph with indent.");
CFAttributedStringReplaceString(attributedString, CFRangeMake(0, 0), string);
CFAttributedStringSetAttribute(attributedString, CFRangeMake(0, CFStringGetLength(string)), kCTParagraphStyleAttributeName, paragraphStyle);
CFRelease(paragraphStyle);

上述代码通过设置 firstLineHeadIndent 为 20.0 和 headIndent 为 10.0,实现了段落的首行缩进和整体缩进。

行距调整

行距对于文本的可读性有很大影响。在 Core Text 中,可以通过 kCTParagraphStyleSpecifierLineSpacing 属性来调整行距。

CGFloat lineSpacing = 5.0;
CTParagraphStyleSetting settings[] = {
    {kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &lineSpacing}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));

CFMutableAttributedStringRef attributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFStringRef string = CFSTR("This is a paragraph with adjusted line spacing.");
CFAttributedStringReplaceString(attributedString, CFRangeMake(0, 0), string);
CFAttributedStringSetAttribute(attributedString, CFRangeMake(0, CFStringGetLength(string)), kCTParagraphStyleAttributeName, paragraphStyle);
CFRelease(paragraphStyle);

这里将行距设置为 5.0,使得文本行与行之间有了额外的间距。

对齐方式

文本的对齐方式包括左对齐、居中对齐、右对齐和两端对齐等。可以通过 kCTParagraphStyleSpecifierAlignment 属性来设置。

CTTextAlignment alignment = kCTCenterTextAlignment;
CTParagraphStyleSetting settings[] = {
    {kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment), &alignment}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));

CFMutableAttributedStringRef attributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFStringRef string = CFSTR("This is a centered paragraph.");
CFAttributedStringReplaceString(attributedString, CFRangeMake(0, 0), string);
CFAttributedStringSetAttribute(attributedString, CFRangeMake(0, CFStringGetLength(string)), kCTParagraphStyleAttributeName, paragraphStyle);
CFRelease(paragraphStyle);

上述代码将文本设置为居中对齐。

文本样式混合

在实际应用中,经常需要在同一文本块中混合不同的文本样式,如部分文本加粗、部分文本变色等。

部分文本加粗

CFMutableAttributedStringRef attributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFStringRef string = CFSTR("This is a normal text, but this part is bold.");
CFAttributedStringReplaceString(attributedString, CFRangeMake(0, 0), string);

CFMutableDictionaryRef fontTraits = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFNumberRef boldTrait = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &kCTFontBoldTrait);
CFDictionarySetValue(fontTraits, kCTFontTraitName, boldTrait);
CFRelease(boldTrait);

CTFontRef originalFont = CTFontCreateWithName(CFSTR("Helvetica"), 16.0, NULL);
CTFontRef boldFont = CTFontCreateCopyWithTraits(originalFont, 0, NULL, fontTraits, NULL);
CFRelease(originalFont);
CFRelease(fontTraits);

CFAttributedStringSetAttribute(attributedString, CFRangeMake(23, 11), kCTFontAttributeName, boldFont);
CFRelease(boldFont);

这段代码创建了一个包含不同样式的文本,通过 CFRange 定位到需要加粗的部分,并应用加粗字体。

部分文本变色

CFMutableAttributedStringRef attributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFStringRef string = CFSTR("This is a normal text, but this part is red.");
CFAttributedStringReplaceString(attributedString, CFRangeMake(0, 0), string);

CGColorRef redColor = CGColorCreateGenericRGB(1.0, 0.0, 0.0, 1.0);
CFAttributedStringSetAttribute(attributedString, CFRangeMake(23, 10), kCTForegroundColorAttributeName, redColor);
CGColorRelease(redColor);

这里通过 kCTForegroundColorAttributeName 属性将部分文本颜色设置为红色。

多行文本与分页

当处理较长文本时,多行显示和分页是必须要考虑的问题。

多行文本布局

在 Core Text 中,只要设置好合适的框架大小,文本会自动进行多行布局。例如:

CFStringRef longString = CFSTR("This is a very long text that will need to be wrapped into multiple lines. Core Text is very good at handling such cases.");
CFAttributedStringRef attributedString = CFAttributedStringCreate(kCFAllocatorDefault, longString, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedString);

CGRect rect = CGRectMake(0, 0, 150, 200);
CGPathRef path = CGPathCreateWithRect(rect, NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);

// 绘制 CTFrame 到视图等操作
// ...

CGPathRelease(path);
CFRelease(frame);
CFRelease(framesetter);
CFRelease(attributedString);

上述代码中,定义了一个较大的矩形框架,Core Text 会自动将长文本进行换行布局。

分页处理

对于更长的文本,可能需要进行分页。可以通过计算文本在不同页面框架中的布局来实现分页。

CFStringRef longString = CFSTR("This is an extremely long text that will span multiple pages. Core Text can handle this with proper pagination.");
CFAttributedStringRef attributedString = CFAttributedStringCreate(kCFAllocatorDefault, longString, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedString);

CGRect pageRect = CGRectMake(0, 0, 200, 300);
NSMutableArray *pageFrames = [NSMutableArray array];
CFRange currentRange = CFRangeMake(0, 0);
do {
    CGPathRef path = CGPathCreateWithRect(pageRect, NULL);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, currentRange, path, NULL);
    [pageFrames addObject:(__bridge id)(frame)];
    CFRelease(frame);
    CGPathRelease(path);

    CTFrameRef lastFrame = (__bridge CTFrameRef)[pageFrames lastObject];
    currentRange = CTFrameGetVisibleStringRange(lastFrame);
    currentRange.location += currentRange.length;
    currentRange.length = 0;
} while (currentRange.location < CFStringGetLength((__bridge CFStringRef)longString));

CFRelease(framesetter);
CFRelease(attributedString);

上述代码通过循环,在不同的页面框架中创建 CTFrame,实现了文本的分页处理。

文本与图形的混合排版

在一些复杂的排版场景中,需要将文本与图形进行混合排版。

在文本中插入图片

可以通过创建一个包含图片的 CTRun 并插入到属性字符串中来实现。假设我们有一个图片 image.png

UIImage *image = [UIImage imageNamed:@"image.png"];
CGImageRef cgImage = image.CGImage;

CTRunDelegateCallbacks callbacks = {0, NULL, NULL, NULL};
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callbacks, (__bridge void *)image);

CFMutableAttributedStringRef attributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFStringRef string = CFSTR("This is text before the image. ");
CFAttributedStringReplaceString(attributedString, CFRangeMake(0, 0), string);

CFMutableAttributedStringRef runString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFStringRef runText = CFSTR(" ");
CFAttributedStringReplaceString(runString, CFRangeMake(0, 0), runText);
CFAttributedStringSetAttribute(runString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
CFAttributedStringReplaceCharacters(attributedString, CFRangeMake(CFStringGetLength(string), 0), runString);
CFRelease(runString);
CFRelease(runDelegate);

CFStringRef postImageString = CFSTR(" This is text after the image.");
CFAttributedStringReplaceString(attributedString, CFRangeMake(CFStringGetLength(string) + 1, 0), postImageString);

上述代码首先创建了一个包含图片的 CTRunDelegate,然后将这个包含图片的 CTRun 插入到属性字符串中合适的位置。

文本环绕图形

要实现文本环绕图形,需要更复杂的布局计算。首先要确定图形的位置和大小,然后调整文本框架的路径。

// 假设图形的位置和大小
CGRect imageRect = CGRectMake(50, 50, 100, 100);

// 创建文本框架路径
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, 200, 300));
CGPathAddRect(path, NULL, imageRect);

CFStringRef longString = CFSTR("This is a long text that should wrap around the image. Core Text can be used to achieve this complex layout.");
CFAttributedStringRef attributedString = CFAttributedStringCreate(kCFAllocatorDefault, longString, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);

// 绘制 CTFrame 等操作
// ...

CFRelease(frame);
CFRelease(framesetter);
CFRelease(attributedString);
CGPathRelease(path);

这里通过在文本框架路径中添加图形的矩形区域,使得文本在排版时会避开图形区域,从而实现文本环绕图形的效果。

Core Text 与 UIKit 的结合

虽然 Core Text 是一个底层框架,但在实际应用中,通常需要与 UIKit 结合来展示文本。

使用 Core Text 绘制文本到 UIView

可以通过在 UIViewdrawRect: 方法中使用 Core Text 进行文本绘制。

@interface MyTextView : UIView
@end

@implementation MyTextView
- (void)drawRect:(CGRect)rect {
    CFStringRef string = CFSTR("Text drawn with Core Text in UIView");
    CFAttributedStringRef attributedString = CFAttributedStringCreate(kCFAllocatorDefault, string, NULL);
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedString);

    CGPathRef path = CGPathCreateWithRect(rect, NULL);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSaveGState(context);
    CTFrameDraw(frame, context);
    CGContextRestoreGState(context);

    CGPathRelease(path);
    CFRelease(frame);
    CFRelease(framesetter);
    CFRelease(attributedString);
}
@end

上述代码创建了一个自定义的 UIView 子类 MyTextView,在 drawRect: 方法中使用 Core Text 绘制文本。

响应 UIKit 事件与 Core Text 文本交互

在某些情况下,需要对 Core Text 绘制的文本进行交互,如点击、选择等。可以通过在 UIView 中添加手势识别器,并结合 Core Text 的文本位置信息来实现。

@interface MyTextView : UIView
@end

@implementation MyTextView
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
        [self addGestureRecognizer:tapGesture];
    }
    return self;
}

- (void)drawRect:(CGRect)rect {
    // Core Text 绘制文本代码
    // ...
}

- (void)handleTap:(UITapGestureRecognizer *)gesture {
    CGPoint tapPoint = [gesture locationInView:self];
    // 根据 Core Text 的 CTFrame 等信息判断点击位置对应的文本
    // 例如获取点击位置对应的字符索引等操作
    // ...
}
@end

这里在自定义的 UIView 中添加了一个点击手势识别器,在手势处理方法中可以根据 Core Text 的相关信息来处理与文本的交互。

通过以上对 Core Text 高级文本排版各个方面的介绍和代码示例,相信你对在 Objective-C 中使用 Core Text 进行复杂文本排版有了更深入的理解。在实际项目中,可以根据具体需求灵活运用这些技术,实现丰富多样的文本展示效果。