Objective-C中的Core Text高级文本排版
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
中的 firstLineHeadIndent
和 headIndent
属性来实现。
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
可以通过在 UIView
的 drawRect:
方法中使用 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 进行复杂文本排版有了更深入的理解。在实际项目中,可以根据具体需求灵活运用这些技术,实现丰富多样的文本展示效果。