CoreText
通过 CTTypesetterRef
、 CTFramesetterRef
、 CTFrameRef
完成文字的排版与布局!
1、 排版类 CTTypesetterRef
typedef const struct CF_BRIDGED_TYPE(id) __CTTypesetter * CTTypesetterRef;
CTTypesetter
是基础的排版类,可以通过富文本创建,并根据需要附加 options
;
该类常用于文本的创建行、执行断行和其它上下文相关的字符处理;(CTLineRef也可以排版,但是只有自己当前行的信息)
1.1、 创建一个排版类
可以根据富文本创建一个排版类
CTTypesetterRef CTTypesetterCreateWithAttributedString(CFAttributedStringRef string);
还可以根据富文本和一些额外配置创建一个排版类
#prama mark - 配置选项
/** 常量:允许无边界的(不限制大小)布局
* @discussion 值为CFBooleanRef 类型,默认值为false;
* 某些文本的 Proper Unicode 布局需要大量工作; 除非将此选项设置为 kCFBooleanTrue,否则此类输入将导致 CTTypesetterCreateWithAttributedStringAndOptions() 返回NULL ;
*/
CT_EXPORT const CFStringRef kCTTypesetterOptionAllowUnboundedLayout;
/** 常量:禁用 Bidi (文本流方向)
* @discussion 值为CFBooleanRef 类型,默认值为false;
* 通常,排版类使用 UAX #9 的 Unicode Bidirectional Algorithm 双向算法。
* 如果在创建排版类时将此选项设置为 true,则不执行方向性重新排序,并且忽略任何方向性控制字符。
*/
CT_EXPORT const CFStringRef kCTTypesetterOptionDisableBidiProcessing;
/** 常量:指定嵌入级别
* @discussion 值为CFNumberRef 类型,默认值为 unset;指定嵌入级别,则忽略任何方向性控制字符。
* 通常,排版类使用 UAX #9 的 Unicode Bidirectional Algorithm 双向算法。
*/
CT_EXPORT const CFStringRef kCTTypesetterOptionForcedEmbeddingLevel;
/** 根据富文本和一些额外配置创建一个排版类
* @param options 额外配置,可以传 NULL
* @return 如果富文本需要进行不合理的操作而无法布局,则可能返回 NULL
*/
CTTypesetterRef _Nullable CTTypesetterCreateWithAttributedStringAndOptions(CFAttributedStringRef string,CFDictionaryRef _Nullable options);
1.2、 创建新行 CTLineRef
/** 使用排版类创建一个新行 CTLineRef
* @param offset 位置偏移量
* @param stringRange 排版类是根据一串富文本创建而来,因此创建 CTLine 时需要根据这串富文本的某段;
* stringRange 就是需要使用的字符串范围,如果 range.length = 0,则该行截止到富文本末尾;
* @note stringRange 的范围不能超过富文本的范围,否则调用将失败。
* @discussion CTLineRef 由按照正确的视觉顺序绘制的字形组成。
*/
CTLineRef CTTypesetterCreateLineWithOffset(CTTypesetterRef typesetter,CFRange stringRange,double offset);
/// 类似于 offset = 0.0 的 CTTypesetterCreateLineWithOffset() 函数
CTLineRef CTTypesetterCreateLine(CTTypesetterRef typesetter,CFRange stringRange);
1.3、 执行断行
/** 根据提供的宽度获取一个换行点
* @param width 请求的断行宽度
* @param offset 位置偏移量
* @param startIndex 换行计算的起始点:将从 startIndex 位置的字符开始,到指定宽度换行;
* @return 返回 startIndex 到换行点之间的字符数量;
* stringRange.location = startIndex && startIndex.length = 返回值,此时可以使用 CTTypesetterCreateLine() 创建一个新行;
* @discussion 可以由富文本中的 '\n' 等触发换行,也可以由指定的宽度触发换行
*/
CFIndex CTTypesetterSuggestLineBreakWithOffset(CTTypesetterRef typesetter,
CFIndex startIndex,double width,double offset);
/// 类似于 offset = 0.0 的 CTTypesetterSuggestLineBreakWithOffset() 函数
CFIndex CTTypesetterSuggestLineBreak(CTTypesetterRef typesetter,CFIndex startIndex,double width);
/// 根据提供的宽度获取一个换行点:可能截断一个字符 ! 类似于 NSLineBreakByCharWrapping
CFIndex CTTypesetterSuggestClusterBreakWithOffset(CTTypesetterRef typesetter,CFIndex startIndex,
double width,double offset);
///类似于 offset = 0.0 的 CTTypesetterSuggestClusterBreakWithOffset() 函数
CFIndex CTTypesetterSuggestClusterBreak(CTTypesetterRef typesetter,CFIndex startIndex,double width );
1.4、 使用 CTTypesetterRef
排版
1.4.1、 使用 CTTypesetterRef
为 CTLine
排版
- (void)drawRect:(CGRect)rect{
//1.获取当前绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext();
//2.旋转坐坐标系(默认和UIKit坐标是相反的)
CGContextSetTextMatrix(context, CGAffineTransformIdentity);//设置当前文本矩阵
CGContextTranslateCTM(context, 0, CGRectGetHeight(rect));//文本沿y轴移动
CGContextScaleCTM(context, 1.0, -1.0);//文本翻转成为CoreText坐标系
NSString *string = @"The 1896 Cedar Keys hurricane was a powerful tropical cyclone that devastated much of the East Coast of the United States, starting with Florida's Cedar Keys, near the end of September. The storm's rapid movement allowed it to maintain much of its intensity after landfall, becoming one of the costliest United States hurricanes at the time. ";
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string attributes:@{NSFontAttributeName: [UIFont fontWithName:@"PingFang SC" size:15],NSForegroundColorAttributeName: [UIColor colorWithRed:51/255.0 green:51/255.0 blue:51/255.0 alpha:1.0]}];
CTTypesetterRef typesetterRef = CTTypesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);///排版类
CFIndex start = 0;
CGPoint textPosition = CGPointMake(0, 55);
double width = CGRectGetWidth(self.bounds);
double height = CGRectGetHeight(self.bounds);
BOOL isCharLineBreak = YES;//断行:是否断字符
BOOL isJustifiedLine = NO; //两端对齐:填充空白符,文字之间等间距
float flush = 0.5;//对齐:0 是左对齐,1 是右对齐,0.5 居中
while (start < string.length) {
CFIndex count;
if (isCharLineBreak) {
count = CTTypesetterSuggestClusterBreak(typesetterRef, start, width);
}else {
count = CTTypesetterSuggestLineBreak(typesetterRef, start, width);
}
CTLineRef line = CTTypesetterCreateLine(typesetterRef, CFRangeMake(start, count));
if (isJustifiedLine) {
line = CTLineCreateJustifiedLine(line, 1, width);
}
double penOffset = CTLineGetPenOffsetForFlush(line, flush, width);
CGContextSetTextPosition(context, textPosition.x + penOffset, height - textPosition.y);
CTLineDraw(line, context);
textPosition.y += CTLineGetBoundsWithOptions(line, 0).size.height;
start += count;
}
}
2、排版生成类 CTFramesetterRef
typedef const struct CF_BRIDGED_TYPE(id) __CTFramesetter * CTFramesetterRef;
排版生成类 CTFramesetterRef
是用来根据富文本生成一个CTFrameRef
的工厂;每个CTFramesetterRef
内都会有一个 CTTypesetterRef
来负责换行、字符处理等。
2.1、创建排版生成类
CTFramesetterRef
既可以根据一个排版类 CTTypesetterRef
创建,也可以根据一段富文本创建:
/** 根据 typesetter 创建一个framesetter对象
* @param typesetter 用于构造 framesetter 的 typesetter
* @result 返回对CTFramesetter对象的引用。
* @discussion 每个 framesetter 在内部使用一个 typesetter 来执行分行等工作;
* 该函数允许使用使用 specific options 创建的typesetter
* @memory 注意合适的时机释放 CFRelease(framesetter);
*/
CTFramesetterRef CTFramesetterCreateWithTypesetter(CTTypesetterRef typesetter);
/** 获取 framesetter 正在使用的 typesetter 对象
* @param framesetter 向其请求的 framesetter
* @memory 该函数获取对 CTTypesetter 对象的引用,调用者不必释放该对象;
*/
CTTypesetterRef CTFramesetterGetTypesetter(CTFramesetterRef framesetter);
/** 根据富文本创建不可变的framesetter对象
* @param attrString 用于构造 framesetter 的富文本
* @result 返回对CTFramesetter对象的引用
* @discussion 生成的 framesetter 对象可用来被 CTFramesetterCreateFrame() 调用创建和填充文本 frames
* @memory 注意合适的时机释放 CFRelease(framesetter);
*/
CTFramesetterRef CTFramesetterCreateWithAttributedString(CFAttributedStringRef attrString);
CTFramesetterRef
使用完之后,要注意它的内存释放问题!
2.2、创建布局CTFrameRef
/** 计算 CTFramesetterRef 所占用的空间大小,方便 CTFramesetterCreateFrame() 所用的CGPath
* @param stringRange 将应用 frame.size 的字符串范围。字符串范围是用于创建framesetter的字符串上的范围。
* 如果 range.length 被设置为 0,那么framesetter将一直添加 CTLineRef ,直到耗尽文本或空间
* @param frameAttributes 在这里指定 frame 填充过程的其他属性,如果没有则为 NULL
* @param constraints 是目标区域的最大size,值为 CGFLOAT_MAX,表示不受限制;
* @param fitRange 受 constrained 限制,最终填充的字符长度
* @result 返回显示字符串所需的实际空间大小
*/
CGSize CTFramesetterSuggestFrameSizeWithConstraints(CTFramesetterRef framesetter,CFRange stringRange,
CFDictionaryRef _Nullable frameAttributes,CGSize constraints,CFRange * _Nullable fitRange);
/** 从 framesetter 创建一个 CTFrameRef
* @param framesetter 用于创建 CTFrame 的 framesetter
* @param stringRange 新frame将基于的字符串范围;字符串范围是用于创建framesetter的字符串上的范围。
* 如果 range.length 被设置为 0,那么framesetter将一直添加 CTLineRef ,直到耗尽文本或空间
* @param path 绘制局域,可以提供一个特殊形状的区域(如圆形、三角形区域等)
* @param frameAttributes 在这里指定 frame 填充过程的其他属性,如果没有则为 NULL
* @result 返回对一个新的CTFrame对象的引用
* @memory 注意合适的时机释放 CFRelease(frameRef);
*/
CTFrameRef CTFramesetterCreateFrame(CTFramesetterRef framesetter,CFRange stringRange,
CGPathRef path,CFDictionaryRef _Nullable frameAttributes);
2.2.1、Demo_1:创建 CTFrame
根据富文本,以及待绘制区域,我们可以创建一个简单的CTFrame
/** 获取 CTFrame
* @param attrString 绘制内容
* @param rect 绘制区域
*/
CTFrameRef getCTFrameWithAttrString(NSAttributedString *attrString, CGRect rect){
CGPathRef path = CGPathCreateWithRect(rect, nil);///绘制局域
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);//设置绘制内容
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil);
CFRelease(framesetter);
CGPathRelease(path);
return frameRef;
}
复杂点的,根据待绘制区域的大小限制,可以创建一个大小合适的CTFrame
/** 获取 CTFrame
* @param attrString 绘制内容
* @param sizeLimit 绘制区域的大小限制
* @param height 设置内容高度,可以用来约束展示内容的 view 的高度
*/
CTFrameRef getCTFrameFitAttrString(NSAttributedString *attrString, CGSize sizeLimit,float *height){
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);//设置绘制内容
CGSize pageSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, sizeLimit, NULL);
*height = pageSize.height;
CGPathRef path = CGPathCreateWithRect(CGRectMake(0, 0, sizeLimit.width, pageSize.height), nil);///绘制局域
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil);
CFRelease(framesetter);
CGPathRelease(path);
return frameRef;
}
2.2.2、Demo_2:获取 CTFrame
大小
根据给出的富文本与宽度限制,我们计算出该富文本渲染在屏幕上所需要的高度:
/** 获取指定内容大小
* @param attrString 内容
* @param widthLimit 宽度限制
*/
CGSize getSizeWithAttributedString(NSAttributedString *attrString,CGFloat widthLimit){
CGSize size = CGSizeZero;
if (attrString.length > 0){
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
size = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(widthLimit, CGFLOAT_MAX), NULL);
CFRelease(framesetter);/// 释放资源
}
return size;
}
3、布局 CTFrameRef
typedef const struct CF_BRIDGED_TYPE(id) __CTFrame * CTFrameRef;
`CTFrame`组成
CTFrameRef
由多个 CTLineRef
组成,有几行文字就有几行 CTLineRef
;
3.1、布局属性 CTFrame~Attribute
/// CTFrameRef 内 CTLineRef 的堆叠方式:水平或垂直堆叠;
/// 垂直堆叠时,在绘图时将使线条逆时针旋转90度;
/// 不同的堆叠方式,并不影响该 CTFrameRef 内字形的外观;
typedef CF_ENUM(uint32_t, CTFrameProgression) {
kCTFrameProgressionTopToBottom = 0, //对于水平文本,行是从上到下堆叠的
kCTFrameProgressionRightToLeft = 1, //垂直文本的行从右到左堆叠
kCTFrameProgressionLeftToRight = 2 //垂直文本的行从左到右堆叠
};
/// CTFrameRef 内 CTLineRef 的堆叠方式:默认值为 kCTFrameProgressionTopToBottom
CT_EXPORT const CFStringRef kCTFrameProgressionAttributeName;
/// 填充规则:当路径与自身相交时,指定填充规则来决定文本被绘制的区域
typedef CF_ENUM(uint32_t, CTFramePathFillRule) {
kCTFramePathFillEvenOdd = 0, // 路径被给定到 CGContextEOFillPath
kCTFramePathFillWindingNumber = 1 // 路径被给定到 CGContextFillPath
};
/** 默认值为 kCTFramePathFillEvenOdd
* @discussion 如果在 frameAttributes 字典的使用此属性,则为 CTFrameRef 指定填充规则;
* 如果在 kCTFrameClippingPathsAttributeName 指定的数组中包含的字典中使用,则为剪切路径指定填充规则;
*/
CT_EXPORT const CFStringRef kCTFramePathFillRuleAttributeName;
/** 默认值为 0
* @discussion 如果在 frameAttributes 字典的使用此属性,则为 CTFrameRef 指定宽度;
* 如果在 kCTFrameClippingPathsAttributeName 指定的数组中包含的字典中使用,则为剪切路径指定宽度;
*/
CT_EXPORT const CFStringRef kCTFramePathWidthAttributeName;
/// 指定 clip frame 的路径数组
CT_EXPORT const CFStringRef kCTFrameClippingPathsAttributeName;
/// 指定 clipping path
CT_EXPORT const CFStringRef kCTFramePathClippingPathAttributeName;
/// 获取用于创建 CTFrameRef 的属性字典,如果没有则返回NULL
CFDictionaryRef _Nullable CTFrameGetFrameAttributes(CTFrameRef frame);
3.1.1、 Demo:根据 CTFrame~Attribute
创建 CTFrameRef
-(CFDictionaryRef)clippingPathsDictionary{
NSMutableArray *pathsArray = [[NSMutableArray alloc] init];
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, 1, -1);
transform = CGAffineTransformTranslate(transform, 0, -self.bounds.size.height);
int eFrameWidth=0;
CFNumberRef frameWidth = CFNumberCreate(NULL, kCFNumberNSIntegerType, &eFrameWidth);
int eFillRule = kCTFramePathFillEvenOdd;
CFNumberRef fillRule = CFNumberCreate(NULL, kCFNumberNSIntegerType, &eFillRule);
int eProgression = kCTFrameProgressionTopToBottom;
CFNumberRef progression = CFNumberCreate(NULL, kCFNumberNSIntegerType, &eProgression);
CFStringRef keys[] = { kCTFrameClippingPathsAttributeName, kCTFramePathFillRuleAttributeName, kCTFrameProgressionAttributeName, kCTFramePathWidthAttributeName};
CFTypeRef values[] = { (__bridge CFTypeRef)(pathsArray), fillRule, progression, frameWidth};
CFDictionaryRef clippingPathsDictionary = CFDictionaryCreate(NULL,
(const void **)&keys, (const void **)&values,
sizeof(keys) / sizeof(keys[0]),
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
return clippingPathsDictionary;
}
- (void)drawRect:(CGRect)rect{
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, 1, -1);
transform = CGAffineTransformTranslate(transform, 0, -rect.size.height);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextConcatCTM(context, transform);
CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)self.attributedString;
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(attributedString);
CFDictionaryRef attributesDictionary = [self clippingPathsDictionary];
CGPathRef path = CGPathCreateWithRect(rect, &transform);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, self.attributedString.length), path, attributesDictionary);
CFRelease(path);
CFRelease(attributesDictionary);
CTFrameDraw(frame, context);
CFRelease(frameSetter);
CFRelease(frame);
}
3.2、获取字符范围
下述两个函数都可以获取 CTFrameRef
的字符范围,返回的 range.location
相同,区别在于 range.length
:
-
CTFrameGetVisibleStringRange()
函数返回的range.length
是该CTFrameRef
内的字符长度; -
CTFrameGetStringRange()
函数返回的range.length =
创建CTFramesetterRef
使用的富文本的长度- range.location
;
/** 获取 CTFrameRef 的字符范围
* 与 CTFrameGetVisibleStringRange() 返回的 range.location 相同,区别在于 range.length
* 该函数返回的 range.length = 创建 CTFramesetterRef 使用的富文本的长度 - range.location
*/
CFRange CTFrameGetStringRange(CTFrameRef frame);
/// 获取实际填充 CTFrameRef 的字符范围
CFRange CTFrameGetVisibleStringRange(CTFrameRef frame);
3.2.1、Demo:文本分页
根据绘制区域 rect
与该区域的 CTFrameRef
,我们可以将很长的富文本分页展示:
/** 根据页面 rect 将文本分页
* @param attrString 内容
* @param rect 显示范围
* @return 返回每页需要展示的 Range
*/
NSMutableArray<NSValue *> *getPageRanges(NSAttributedString *attrString, CGRect rect){
NSMutableArray *rangeArray = [NSMutableArray array];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
CGPathRef path = CGPathCreateWithRect(rect, nil);
CFRange range = CFRangeMake(0, 0);
NSInteger rangeOffset = 0;
do {
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(rangeOffset, 0), path, nil);
range = CTFrameGetVisibleStringRange(frame);
[rangeArray addObject:[NSValue valueWithRange:NSMakeRange(rangeOffset, range.length)]];
CFRelease(frame);
rangeOffset += range.length;
} while (range.location + range.length < attrString.length);
CFRelease(framesetter);
CGPathRelease(path);
return rangeArray;
}
文本分页模型
@interface YLPageModel : NSObject
/// 当前页富文本
@property (nonatomic, strong) NSAttributedString *content;
/// 当前页码
@property (nonatomic, assign) NSInteger page;
/// 当前页文字范围
@property (nonatomic, assign) NSRange range;
/// 当前页 CTFrame
@property (nonatomic ,assign) CTFrameRef frameRef;
/// 当前页高度
@property (nonatomic, assign) CGFloat contentHeight;
@end
根据每页所能展示的字符范围 range
,创建分页模型
/** 将内容分为多页
* @param attrString 展示的内容
* @prama rect 显示范围
*/
NSMutableArray<YLPageModel *> * getPageModels(NSMutableAttributedString *attrString, CGRect rect){
NSMutableArray<YLPageModel *> *pageModels = [NSMutableArray array];
[getPageRanges(attrString, rect) enumerateObjectsUsingBlock:^(NSValue * _Nonnull value, NSUInteger idx, BOOL * _Nonnull stop) {
YLPageModel *pageModel = [[YLPageModel alloc]init];
pageModel.range = value.rangeValue;
pageModel.content = [attrString attributedSubstringFromRange:pageModel.range];
pageModel.page = idx;
float height;
pageModel.frameRef = getCTFrameFitAttrString(pageModel.content, rect.size, &height);
pageModel.contentHeight = height;
[YLCoreText setImageFrametWithCTFrame:pageModel.frameRef];
[pageModels addObject:pageModel];
}];
return pageModels;
}
3.3、获取路径 CGPathRef
///获取用于创建 CTFrameRef 的路径
CGPathRef CTFrameGetPath(CTFrameRef frame);
3.4、获取布局CTFrameRef
中的行CTLineRef
/// 获取组成 CTFrameRef 的所有行(CTLineRef 对象),可能返回空数组
CFArrayRef CTFrameGetLines(CTFrameRef frame);
/** 拷贝 CTFrameRef 中指定范围的所有行的原点 origin
* @param range 希望拷贝的范围。如果 range.length 设置为0,则将从 range.location 拷贝至最后一行
* @param origins 缓冲区,用于存储待复制的数据;缓冲区的长度至少要大于待拷贝的数据份数;
* @note 当使用 origin 来计算 CTFrameRef 内容的字形度量时,请注意线的原点并不总是对应于线度量;例如,段落样式设置可以影响线条的 origin ;
* @discussion 数组 origins 的最大存储量是行数组的计数;
*/
void CTFrameGetLineOrigins(CTFrameRef frame,CFRange range,CGPoint origins[_Nonnull]);
3.4.1、Demo:计算CTFrame
的内容高度
通过遍历CTFrame
上所有的 CTLine
,可以计算出图文的纯高度,此时需要注意 coreText 的坐标系与 UIKit 的坐标系不统一问题:
/** 获取 CTFrameRef 的内容高度
* @note CTFrameRef 的坐标系是以左下角为原点
*/
CGFloat getHeightWithCTFrame(CTFrameRef frameRef){
CFArrayRef lines = CTFrameGetLines(frameRef);
int lineCount = (int)CFArrayGetCount(lines);
CGPoint origins[lineCount];//以左下角为原点的坐标系
for (int i = 0; i < lineCount; i++) {
origins[i] = CGPointZero;
}
CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), origins);
CGPoint point = origins[lineCount - 1];//最后一行的 point.y 是最小值
CGFloat lineAscent = 0; //上行高度
CGFloat lineDescent = 0; //下行高度
CGFloat lineLeading = 0; //行距
CTLineRef line = CFArrayGetValueAtIndex(lines, lineCount - 1);
CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);
/// 获取该页面的高度 pageHeight
CGPathRef path = CTFrameGetPath(frameRef);
CGRect bounds = CGPathGetBoundingBox(path);
CGFloat pageHeight = CGRectGetHeight(bounds);
/// 空白高度 = point.y 是最小值 - 下行高度 - 行距
/// 内容高度 = 页面高度 - 空白高度
return pageHeight - (point.y - ceil(lineDescent) - lineLeading);
}
3.5、绘制布局CTFrameRef
/** 将 CTFrameRef 绘制到上下文 CGContext 中;
* @note 该调用可能会使上下文处于任何状态,并且在绘制操作之后不会刷新它。
*/
void CTFrameDraw(CTFrameRef frame,CGContextRef context);
第一篇 CoreText的简单了解
第二篇 CoreText 排版与布局
第三篇 CTLineRef 的函数库及使用
第四篇 图文混排的关键 CTRunRef 与 CTRunDelegate
Demo:小说阅读器的文字分页、图文混排
网友评论