美文网首页
iOS 文本相关-CoreText

iOS 文本相关-CoreText

作者: forping | 来源:发表于2021-01-01 00:16 被阅读0次

Core Text is an advanced, low-level technology for laying out text and handling fonts. Core Text works directly with Core Graphics (CG), also known as Quartz, which is the high-speed graphics rendering engine that handles two-dimensional imaging at the lowest level in OS X and iOS.
CoreText是一种高级的底层技术, 用于布局文本和处理字体。CoreText直接与Core Graphics (CG) 一起工作, 也称为Quartz, 它是在 OS X 和 iOS 的最底层的处理二维成像的高速图形渲染引擎。

相关知识

字体(Font)

字体表示的是同一大小,同一样式(Style)字形的集合。从这个意义上来说,当我们为文字设置粗体,斜体时其实是使用了另外一种字体(下划线不算)。

字符(Character)和字形(Glyphs)

排版过程中一个重要的步骤就是从字符到字形的转换,字符表示信息本身,一般就是指某种编码,如Unicode编码,而字形是它的图形表现形式,指字符编码对应的图片。但是他们之间不是一一对应关系,同个字符的不同字体族,不同字体大小,不同字体样式都对应了不同的字形。而由于连写(Ligatures)的存在,多个字符也会存在对应一个字形的情况。


image.png

字形的各个参数(字形度量Glyph Metrics)

image.png
  • 边界框 bbox(bounding box)
    这是一个假想的框子,它尽可能紧密的装入字形。
  • 基线(baseline)
    一条假想的线,一行上的字形都以此线作为上下位置的参考,在这条线的左侧存在一个点叫做基线的原点,
  • 上行高度(ascent)
    从原点到字体中最高(这里的高深都是以基线为参照线的)的字形的顶部的距离,ascent是一个正值
  • 下行高度(descent)
    从原点到字体中最深的字形底部的距离,descent是一个负值(比如一个字体原点到最深的字形的底部的距离为2,那么descent就为-2)
  • 行距(line gap)
    line gap也可以称作leading(其实准确点讲应该叫做External leading),行高line Height则可以通过 ascent + |descent| + linegap 来计算。
  • 字间距(Kerning)
    字与字之间的距离,为了排版的美观,并不是所有的字形之间的距离都是一致的,但是这个基本步影响到我们的文字排版。
  • 基础原点(Origin)
    基线上最左侧的点。
image.png

红框高度既为当前行的行高,绿线为baseline,绿色到红框上部分为当前行的最大Ascent,绿线到黄线为当前行的最大Desent,而黄框的高即为行间距。
由此可以得出:lineHeight = Ascent + |Decent| + Leading

坐标系

CoreText一开始是定位于桌面的排版系统,使用了传统的原点在左下角的坐标系,所以它在绘制文本的时候都是参照左下角的原点进行绘制的。
Core Graphic 中的context也是以左下角为原点的,但通过UIGraphicsGetCurrentContext()获得的当前context是已经被处理过的了,如果啥也不处理,直接在当前context上进行CoreText绘制,文字是镜像且上下颠倒,因此在CoreText中的布局需要对其坐标系进行转换,。一般做法是直接获取当前上下文。并将当前上下文的坐标系转换为CoreText坐标系,再将布局好的CoreText绘制到当前上下文中即可。

使用以下的代码进行坐标系的转换

// 坐标系调整
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);//设置字形变换矩阵为CGAffineTransformIdentity,也就是说每一个字形都不做图形变换

//    CGContextTranslateCTM(context, 0, rect.size.height);
//    CGContextScaleCTM(context, 1, -1);
CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, rect.size.height));

CoreText框架中重要的类

image.png
  • CFAttributedStringRef
    属性字符串,用于存储需要绘制的文字字符和字符属性

  • CTFramesetterRef
    framesetter对应的类型是 CTFramesetter,通过CFAttributedStringRef进行初始化,它作为CTFrame对象的生产工厂,负责根据path生产对应的CTFrame;

  • CTFrame
    CTFrame是可以通过CTFrameDraw函数直接绘制到context上的,也可以在绘制之前,操作CTFrame中的CTLine,进行参数的微调。

  • CTLine
    在CTFrame内部是由多个CTLine来组成的,每个CTLine代表一行;可以看做Core Text绘制中的一行的对象 通过它可以获得当前行的line ascent,line descent ,line leading,还可以获得Line下的所有Glyph Runs;

  • CTRun
    或者叫做 Glyph Run,每个CTLine又是由多个CTRun组成的,每个CTRun代表一组显示风格一致的文本,是一组共享相同attributes(属性)的字形的集合体;

CTFrame是指整个该UIView子控件的绘制区域,CTLine则是指每一行,CTRun则是每一段具有一样属性的字符串。比如某段字体大小、颜色都一致的字符串为一个CTRun,CTRun不可以跨行,不管属性一致或不一致。通常的结构是每一个CTFrame有多个CTLine,每一个CTLine有多个CTRun。
你不需要自己创建CTRun,Core Text将根据NSAttributedString的属性来自动创建CTRun。每个CTRun对象对应不同的属性,正因此,你可以自由的控制字体、颜色、字间距等等信息。

image.png
CTFrameRef

如上图中最外层(蓝色框)的内容区域对应的就是CTFrame,绘制的是一整段的内容,方法的汇总

/*
返回CTFrameRef对象的CFType
类型ID是一个整数,用于标识Core Foundation对象“所属”的不透明类型。
*/
CFTypeID CTFrameGetTypeID( void );
/*
返回最初要求填充框架的字符范围。
*/
CFRange CTFrameGetStringRange(CTFrameRef frame )

/*
返回实际适合frame的字符范围。
此函数可用于级联frame,因为它返回可以在Frame中看到的字符范围。 下一个Frame将从该Frame结束处开始。
*/
CFRange CTFrameGetVisibleStringRange( CTFrameRef frame ) ;
// 返回用于创建frame的path
CGPathRef CTFrameGetPath(CTFrameRef frame )
// 返回用于创建frame的Attributes。
CFDictionaryRef _Nullable CTFrameGetFrameAttributes(CTFrameRef frame );

// 返回frame中的CTLine的数组。
CFArrayRef CTFrameGetLines(CTFrameRef frame )

/*
获取CTFrame中每一行的起始坐标,结果保存在返回参数中

将一些CGPoint结构体复制到原点缓冲区中。复制到原点缓冲区的最大行原点数是range参数的长度。
frame
您要从中获取线原点数组的frame。
range
复制的线起点范围。如果range的长度为0,则将从range的location起到最后的行原点。
origins
将原点复制到的缓冲区。具有至少与range.length一样多的元素。此数组中的每个CGPoint都是CTFrameGetLines返回的相对于路径边界框原点的线数组中对应线的原点,可以从CGPathGetPathBoundingBox获得该点。
*/
void CTFrameGetLineOrigins(CTFrameRef frame,CFRange range,CGPoint origins[_Nonnull] ) 

//将整个frame绘制到上下文中。
void CTFrameDraw( CTFrameRef frame, CGContextRef context )
CTFramesetterRef
// 返回框架设置器对象的CFType
CFTypeID CTFramesetterGetTypeID( void );
/*
通过从CTTypesetterRef创建CTFramesetterRef。
*/
CTFramesetterRef CTFramesetterCreateWithTypesetter(CTTypesetterRef typesetter )

/*
从属性字符串创建不可变的CTFramesetterRef对象。

生成的CTFramesetterRef可用于通过CTFramesetterCreateFrame调用创建和填充CTFrame。
*/
CTFramesetterRef CTFramesetterCreateWithAttributedString(CFAttributedStringRef attrString )

/*
使用CTFramesetterRef创建不可变CTFrameRef。

创建一个由path参数提供的路径形状的,充满了字形的CTFrame。framesetter将继续填充frame,直到用完文本或发现不再适合的文本为止。

framesetter
用来创建CTFrame的CTFramesetterRef。
stringRange
创建framesetter的属性字符串的范围,该范围将在适合框架的行中排版。如果ranfe的length部分设置为0,则framesetter将继续添加行,直到用完文本或空格为止。
path
指定frame形状的CGPath对象。该路径可能不是矩形的。
frameAttributes
可以在此处指定控制帧填充过程的其他属性,如果没有这样的属性,则为NULL。
*/
CTFrameRef CTFramesetterCreateFrame(CTFramesetterRef framesetter, CFRange stringRange, CGPathRef path,CFDictionaryRef _Nullable frameAttributes )

/*
返回CTFramesetterRef正在使用的CTTypesetterRef对象。

每个CTFramesetterRef在内部使用CTTypesetterRef根据字符串中的字符进行换行和其他上下文分析; 如果调用者想对该CTTypesetterRef执行其他操作,此函数将返回特定CTFramesetterRef正在使用的CTTypesetterRef。

*/
CTTypesetterRef CTFramesetterGetTypesetter( CTFramesetterRef framesetter )
/*
确定字符串范围所需的 CTFrame大小。

framesetter
用于测量框架尺寸。
stringRange
CTFrame大小适用的字符串范围。字符串范围是指用于创建framesetter的字符串范围。如果range的length部分设置为0,则framesetter将继续添加line,直到用完文本或空格为止。
frameAttributes
控制CTFrame填充过程的其他属性,如果没有这样的属性,则为NULL。
constraints
CTFrame尺寸受其限制的宽度和高度。任一维度的CGFLOAT_MAX值都表示应将其视为不受约束。
fitRange
返回时,包含实际适合约束大小的字符串范围。
*/
CGSize CTFramesetterSuggestFrameSizeWithConstraints( CTFramesetterRef framesetter,CFRange stringRange,CFDictionaryRef _Nullable frameAttributes,CGSize constraints,  CFRange * _Nullable fitRange )
CTLine

上图红色框中的内容就是CTLine,上图一共有三个CTLine对象

// 返回CTLine对象的Core Foundation类型标识符。
CFTypeID CTLineGetTypeID( void );
/*
直接从属性字符串创建单个不可变的行对象。

允许客户端创建行而不创建CTTypesetter对象。  不需要换行符的简单元素(例如文本标签)可以使用此API。
*/
CTLineRef CTLineCreateWithAttributedString(CFAttributedStringRef attrString );

/*
从现有行创建截断的行。
line
创建截断行的行。
width
截断开始处的宽度。如果line的宽度大于width,则该行将被截断。
truncationType 截断类型
truncationToken
该CTLineRef被添加到发生截断的位置,以指示该行已被截断。 通常,截断令牌是省略号(U + 2026)。 如果此参数设置为NULL,则不使用截断令牌,仅将行切断。
*/ 
CTLineRef _Nullable CTLineCreateTruncatedLine( CTLineRef line, double width, CTLineTruncationType truncationType, CTLineRef _Nullable truncationToken )
typedef CF_ENUM(uint32_t, CTLineTruncationType) {
    kCTLineTruncationStart  = 0,// 在行的开头截断,使结束部分可见。
    kCTLineTruncationEnd    = 1, // 在行尾截断,使开始部分可见。
    kCTLineTruncationMiddle = 2 // 在行的中间截断,使开始和结束部分都可见。
};
/*
从现有线创建对齐线。
line
从中创建对齐线的线。
justificationFactor
设置为1.0或更大时,将执行完全对齐。 如果将此参数设置为小于1.0,则执行不同程度的部分对齐。 如果将其设置为0或小于0,则不进行对齐。
justificationWidth
所得行对齐的宽度。 如果justificationWidth小于线条的实际宽度,则执行负对齐(即,字形被挤压在一起)。
*/
CTLineRef _Nullable CTLineCreateJustifiedLine( CTLineRef line, CGFloat justificationFactor, double justificationWidth )
// 返回line的总字形计数。
// 总字形计数等于line的字形的总和。
CFIndex CTLineGetGlyphCount( CTLineRef line );

// 返回运行时line对象的字形数组。获取CTLine包含的所有的CTRun
CFArrayRef CTLineGetGlyphRuns( CTLineRef line )
// 获取该行中最初产生字形的字符范围。
CFRange CTLineGetStringRange( CTLineRef line )
/*
获取绘制平齐文本所需的笔偏移量。
line
从中获得平齐位置的线。
flushFactor
flushFactor等于或小于0表示左冲洗。 等于或大于1.0的flushFactor表示正确冲洗。 冲洗系数在0到1.0之间表示中心冲洗程度不同,值为0.5表示完全中心冲洗。
flushWidth
指定冲洗操作的宽度。
*/
double CTLineGetPenOffsetForFlush( CTLineRef line, CGFloat flushFactor, double flushWidth )
//画一条完整的线。
/*
这是一个方便的函数,因为可以通过运行字形,从中删除字形并调用诸如CGContextShowGlyphsAtPositions之类的功能来逐行绘制线。
*/
void CTLineDraw(CTLineRef line, CGContextRef context )
/*
计算线条的印刷范围。
line
计算其印刷范围的线。
ascent
线的上升。 如果不需要,可以将此参数设置为NULL。
descent
线的下降。 如果不需要,可以将此参数设置为NULL。
leading
该行的前导。 如果不需要,可以将此参数设置为NULL。
*/
double CTLineGetTypographicBounds( CTLineRef line, CGFloat * _Nullable ascent, CGFloat * _Nullable descent, CGFloat * _Nullable leading )

/*
计算一条线的rect。
CTLineBoundsOptions
*/
CGRect CTLineGetBoundsWithOptions( CTLineRef line,CTLineBoundsOptions options )

//传递0(无选项)将返回印刷范围,
typedef CF_OPTIONS(CFOptionFlags, CTLineBoundsOptions) {
    kCTLineBoundsExcludeTypographicLeading  = 1 << 0,//传递此选项可排除印刷前导。
    kCTLineBoundsExcludeTypographicShifts   = 1 << 1,// 传递此选项可忽略由于定位(例如字距调整或基线对齐)而引起的跨流移位。
    kCTLineBoundsUseHangingPunctuation      = 1 << 2, // 通常,线边界包括所有字形;将此选项传递为将标准标点悬挂在行的任何一端,视为完全悬挂。
    kCTLineBoundsUseGlyphPathBounds         = 1 << 3, // 传递此选项以使用字形路径范围,而不是默认的印刷范围。
    kCTLineBoundsUseOpticalBounds           = 1 << 4, // 传递此选项以使用光学范围。此选项优先 kCTLineBoundsUseGlyphPathBounds。
    kCTLineBoundsIncludeLanguageExtents     = 1 << 5,// 传递此选项可根据各种语言的通用字形序列添加额外的空间。该结果打算在绘制时使用,以避免可能由于印刷范围引起的剪裁。与kCTLineBoundsUseGlyphPathBounds一起使用时,此选项无效。
};

/*
返回行的尾随空白宽度。根据尾随空白宽度创建线条可能导致线条实际上比所需宽度更长。 
由于不可见空格而通常这不是问题,但此函数可用于确定由于尾随空格而导致的行宽量。
*/
double CTLineGetTrailingWhitespaceWidth( CTLineRef line )

/*
计算线条的图像边界。
line
计算图像边界的线。
context
计算图像边界的上下文。 这是必需的,因为上下文中可能包含一些设置,这些设置会导致图像边界发生变化。
*/
CGRect CTLineGetImageBounds(CTLineRef line,CGContextRef _Nullable context )

/*
执行命中测试。用于确定鼠标单击或其他事件的字符串索引。 该字符串索引对应于下一个字符应插入的字符。 
line
正在检查的线。
position
鼠标单击相对于线条原点的位置。
*/ 
CFIndex CTLineGetStringIndexForPosition( CTLineRef line, CGPoint position );

/*
确定字符串索引的图形偏移量。获取CTRun的起始位置

此函数返回一个或多个与字符串索引对应的图形偏移,适用于相邻线之间的移动或绘制自定义插入记号。
为了在相邻的线之间移动,可以针对两条线的任何相对压痕来调整主偏移量。
构造x值为偏移,其y值为0.0的CGPoint适合传递给CTLineGetStringIndexForPosition。
对于绘制自定义插入符号,返回的主偏移量对应于插入符号的一部分,该部分代表字符的视觉插入位置,该字符的方向与行的书写方向匹配。
line
请求偏移量的行。
charIndex
对应于所需位置的字符串索引。
secondaryOffset
在输出时,沿charIndex的基线的辅助偏移量。当单个插入符足以容纳字符串索引时,该值将与主偏移量相同,主偏移量是该函数的返回值。可能为NULL。
*/ 
CGFloat CTLineGetOffsetForStringIndex( CTLineRef line, CFIndex charIndex, CGFloat * _Nullable secondaryOffset )

// 枚举一行中字符的插入符偏移量。
void CTLineEnumerateCaretOffsets(  CTLineRef line,  void (^block)(double offset, CFIndex charIndex, bool leadingEdge, bool* stop) )
CTRun

如上图绿色框中的内容就是CTRun,每一行中相同格式的一块内容是一个CTRun,一行中可以存在多个CTRun

// 返回CTRunRef对象的Core Foundation类型标识符。
CFTypeID CTRunGetTypeID( void )
// 获取CTRunRef的字形计数。
CFIndex CTRunGetGlyphCount( CTRunRef run ) 
/*返回用于创建字形运行的属性字典。
获取到的内容是通过`CFAttributedStringSetAttribute`方法设置给属性字符串的NSDictionary,
key为`kCTRunDelegateAttributeName`,值为`CTRunDelegateRef`*/
CFDictionaryRef CTRunGetAttributes( CTRunRef run )
/*
CTRunRef具有可用于加快某些操作的状态。 
知道CTRunRef字形的方向和顺序可以帮助进行字符串索引分析,
而知道位置是否引用身份文本矩阵可以避免进行昂贵的比较。 
提供此状态是为了方便,因为此信息不是严格必需的,但在某些情况下可能会有所帮助。
*/
CTRunStatus CTRunGetStatus(CTRunRef run )
//用于指示运行的处置。
typedef CF_OPTIONS(uint32_t, CTRunStatus)
{
    kCTRunStatusNoStatus = 0,// 没有特殊属性。
    kCTRunStatusRightToLeft = (1 << 0),//设置后,运行从右到左。
/*
设置后,游程已以某种方式重新排序,使得与字形关联的字符串索引不再严格增加(对于从左至右游程)或减少(对于从右至左游程)。
*/
    kCTRunStatusNonMonotonic = (1 << 1),
    kCTRunStatusHasNonIdentityMatrix = (1 << 2) //设置后,运行需要在当前CG上下文中设置特定的文本矩阵才能正确绘制。
};
/*
返回CTRunRef中存储的字形数组的直接指针。
字形数组的长度等于CTRunGetGlyphCount返回的值。 即使流中包含字形,调用者也应为此函数做好准备以返回NULL。 如果此函数返回NULL,则调用者必须分配自己的缓冲区并调用CTRunGetGlyphs以获取字形。
*/
const CGGlyph * _Nullable CTRunGetGlyphsPtr(
    CTRunRef run )

/*
将一系列字形复制到用户提供的缓冲区中。
run
从其复制字形的CTRunRef。
range
要复制的字形范围。 如果范围的长度设置为0,则复制操作将从范围的开始索引继续到运行结束。
buffer
字形复制到的缓冲区。 必须至少将缓冲区分配给范围长度指定的值。
*/
void CTRunGetGlyphs( CTRunRef run, CFRange range, CGGlyph buffer[_Nonnull] )

/*
返回CTRunRef中存储的字形位置数组的直接指针。

相对于包含CTRunRef的CTLineRef的原点。CTRunRef中字形的位置
 位置数组的长度等于CTRunGetGlyphCount返回的值。 
即使流中包含字形,调用者也应为此函数做好准备以返回NULL。 
如果此函数返回NULL,则调用者必须分配自己的缓冲区并调用CTRunGetPositions以获取字形位置。
*/ 
const CGPoint * _Nullable CTRunGetPositionsPtr( CTRunRef run )

/*
返回CTRunRef中存储的 字形提前 数组的直接指针。

数组的长度等于CTRunGetGlyphCount返回的值。 
即使流中包含字形,调用者也应为此函数做好准备以返回NULL。 
如果此函数返回NULL,则调用者需要分配自己的缓冲区并调用CTRunGetAdvances以获取。 请注意,仅前进就不足以在行中正确定位字形,因为CTRunRef可能具有非同一性矩阵,或者CTline中的初始字形可能具有非零原点。 调用者应考虑改用positions代替。
*/ 
const CGSize * _Nullable CTRunGetAdvancesPtr( CTRunRef run )
/*
将一定范围的字形前进复制到用户提供的缓冲区中。

run
复制的CTRunRef。
range
希望复制的字形扩展范围。 如果范围的长度设置为0,则复制操作将从范围的开始索引继续到运行结束。
buffer
字形前进到的缓冲区被复制。 必须至少将缓冲区分配给范围长度指定
*/
void CTRunGetAdvances( CTRunRef run,  CFRange range, CGSize buffer[_Nonnull] )
/*
返回运行中存储的字符串索引的直接指针。

索引是最初生成CTRunRef的字形的字符索引。 它们可用于将CTRunRef中的字形映射到后备存储中的字符。 
字符串索引数组的长度将等于CTRunGetGlyphCount返回的值。 
即使流中包含字形,调用者也应为此函数做好准备以返回NULL。 
如果此函数返回NULL,则调用者必须分配自己的缓冲区并调用CTRunGetStringIndices来获取索引。
*/
const CFIndex * _Nullable CTRunGetStringIndicesPtr(CTRunRef run )
/*
将一系列字符串索引复制到用户提供的缓冲区中。
*/
void CTRunGetStringIndices( CTRunRef run,CFRange range,CFIndex buffer[_Nonnull] )

// 获取CTRunRef最初产生字形的字符范围。获取CTRun字符串的Range
CFRange CTRunGetStringRange( CTRunRef run )

/*
获取CTRun的绘制属性`ascent`、`desent`,返回值是CTRun的宽度:(印刷范围)。

run
计算印刷范围的CTRunRef。
range
要测量的CTRunRef范围。 如果范围的长度设置为0,则测量操作将从范围的起始索引继续到运行结束。
ascent
运行上升。 如果不需要,可以将其设置为NULL。
descent
运行下降。 如果不需要,可以将其设置为NULL。
leading
运行的领先。 如果不需要,可以将其设置为NULL。
*/
double CTRunGetTypographicBounds(CTRunRef run, CFRange range, CGFloat * _Nullable ascent,CGFloat * _Nullable descent,  CGFloat * _Nullable leading )

/*
计算字形范围的图像边界。
run
计算图像边界的CTRunRef。
context
正在计算图像边界的上下文。 这是必需的,因为上下文中可能包含一些设置,这些设置会导致图像边界发生变化。
range
要测量的CTRunRef范围。 如果范围的长度设置为0,则测量操作将从范围的起始索引继续到CTRunRef结束。
*/
CGRect CTRunGetImageBounds(CTRunRef run, CGContextRef _Nullable context,CFRange range )
// 返回绘制此CTRunRef所需的文本矩阵。
// 为了在CTRunRef中正确绘制字形,应将此函数返回的CGAffineTransform的字段tx和ty设置为当前文本位置。
CGAffineTransform CTRunGetTextMatrix(
    CTRunRef run )
/*
将一定范围的基础前进和起点复制到用户提供的缓冲区中。
CTRunRef的基本前进和起点确定其字形的位置,但在用于绘制之前需要进行其他处理。
与CTRunGetAdvances返回的前进类似,基本前进是从字形的原点到下一个字形的原点的位移,除了基本前进不包括字体布局表相对于另一个字形所做的任何定位(例如相对于其基准的标记)。
当前字形的原点相对于起始位置的偏移量决定了字形的实际位置,当前字形的基本前进距离相对于起始位置的偏移量确定了下一个字形的位置。

runRef
包含要复制的基本进度和起点的CTRunRef。
range
要复制的值的范围。如果范围的长度设置为0,则复制操作将从范围的起始索引继续到运行结束。
advancesBuffer
基本行进将被复制到的缓冲区,或者为NULL。如果不为NULL,则缓冲区必须允许至少与范围长度指定的元素一样多的元素。
originsBuffer
将原点复制到的缓冲区,或者为NULL。如果不为NULL,则缓冲区必须允许至少与范围长度指定的元素一样多的元素。
*/
void CTRunGetBaseAdvancesAndOrigins( CTRunRef runRef, CFRange range, CGSize advancesBuffer[_Nullable], CGPoint originsBuffer[_Nullable] )

/*
绘制完整的CTRunRef或一部分CTRunRef。

range
要绘制的部分。 如果范围的长度设置为0,则绘制操作将从范围的起始索引继续到运行结束。
*/
void CTRunDraw(CTRunRef run, CGContextRef context,CFRange range )
CTRunDelegate

CTRunDelegate和CTRun是紧密联系的,CTFrame初始化的时候需要用到的图片信息是通过CTRunDelegate的callback获得到的,

// 返回CTRunDelegate对象的CFType。
CFTypeID CTRunDelegateGetTypeID( void )
/* 定义释放CTRunDelegate对象时调用的函数的指针。

refCon
创建CTRunDelegate时,提供给CTRunDelegateCreate函数的引用常数
*/
typedef void (*CTRunDelegateDeallocateCallback) (void * refCon );
/*
指向函数的指针,该函数确定CTRun中字形的印刷升序。
*/
typedef CGFloat (*CTRunDelegateGetAscentCallback) (void * refCon );
/*
指向函数的指针,该函数确定CTRun中字形的印刷降序。
*/
typedef CGFloat (*CTRunDelegateGetDescentCallback) ( void * refCon );
// 指向函数的指针,该函数确定运行中字形的印刷宽度。
typedef CGFloat (*CTRunDelegateGetWidthCallback) (void * refCon );
/*
创建CTRunDelegateRef的不可变实例。需要传递CTRunDelegateCallbacks对象,使用CFAttributedStringSetAttribute方法把CTRunDelegate对象和NSAttributedString对象绑定,在CTFrame初始化的时候回调用CTRunDelegate对象里面CTRunDelegateCallbacks对象的回调方法返回`Ascent`、`Descent`、`Width`信息

callbacks
一个结构,它包含指向此运行委托的回调的指针。
*/
CTRunDelegateRef _Nullable CTRunDelegateCreate(const CTRunDelegateCallbacks* callbacks, void * _Nullable refCon )

typedef struct
{
    CFIndex                         version;
    CTRunDelegateDeallocateCallback dealloc;
    CTRunDelegateGetAscentCallback  getAscent;
    CTRunDelegateGetDescentCallback getDescent;
    CTRunDelegateGetWidthCallback   getWidth;
} CTRunDelegateCallbacks;

// 返回 CTRunDelegate 的“ refCon”值。
void * CTRunDelegateGetRefCon(CTRunDelegateRef runDelegate )

绘制操作

简单绘制文字
image.png
- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    // 坐标系调整
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, rect.size.height));
    
    // 路径
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, rect);
    
    // 绘制的内容属性字符串
     NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:18],
                                  NSForegroundColorAttributeName: [UIColor blueColor]
                                  };
    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:@"你的回话凌乱着 在这个时刻\
我想起喷泉旁的白鸽 甜蜜散落了\
情绪莫名的拉扯 我还爱你呢\
而你断断续续唱着歌 假装没事了\
时间过了 走了 爱情面临选择\
你冷了 倦了 我哭了\
离开时的不快乐 你用卡片手写着\
有些爱只给到这 真的痛了" attributes:attributes];

    // 使用NSMutableAttributedString创建CTFrame,并绘制
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
    
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
    CTFrameDraw(frame, context);
}

效果


image.png
竖版文本绘制

待续

绘制图片

Core Text本身并不支持图片绘制,图片的绘制需要通过Core Graphics进行。
通过设置CTRun为图片的绘制中留出适当的空间,需要使用到CTRunDelegate了,CTRunDelegate作为CTRun相关属性或操作扩展的一个入口,使得我们可以对CTRun做一些自定义的行为。
为图片留位置的方法就是加入一个空白的CTRun,自定义其ascent,descent,width等参数,使得绘制文本的时候留下空白位置给相应的图片。然后图片在相应的空白位置上使用Core Graphics接口进行绘制。
因此绘制图片最重要的一个步骤就是计算图片所在的位置,最后是在drawRect绘制方法中使用CGContextDrawImage方法进行绘制图片即可

计算图片位置流程图
image.png

效果图


image.png
主要代码

创建需要渲染的属性字符串,和渲染

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1, -1);

    NSMutableAttributedString *attri = [[NSMutableAttributedString alloc] initWithString:@"每个人都认为他自己至少具有一种主要的美德,我的美德是:我是我所结识过的少有的几个诚实人中间的一个。"];

    [attri appendAttributedString:[self imageAttributeString]];
    
    [attri appendAttributedString:[[NSAttributedString alloc] initWithString:@"人们的品行有的好像建筑在坚硬的岩石上,有的好像建筑在泥沼里,不过超过一定的限度,我就不在乎它建在什么之上了。"]];
    
    [attri addAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:17.0f]} range:NSMakeRange(0, attri.length)];
    // 103 × 150
    
    
    CTFrameRef frame = [self ctFrameWithAttributeString:attri.copy frame:rect];
    
    [self drawWithFrame:frame context:context];
}

创建图片的属性字符串

/*
1. 创建CTRunDelegate对象,传递callback和参数的代码,
创建CTFrame对象的时候会通过CTRunDelegate中callbak的几个回调方法
getDescent、getDescent、getWidth返回绘制的图片的信息,
方法getDescent、getDescent、getWidth中的参数是
CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData))方法中的metaData参数,
特别地,这里的参数需要把所有权交给CF对象,而不能使用简单的桥接,防止ARC模式下的OC对象自动释放,
在方法getDescent、getDescent、getWidth访问会出现BAD_ACCESS的错误
*/
- (NSAttributedString *)imageAttributeString {
    // 1 创建CTRunDelegateCallbacks
    CTRunDelegateCallbacks callback;
    memset(&callback, 0, sizeof(CTRunDelegateCallbacks));
    callback.getAscent = getAscent;
    callback.getDescent = getDescent;
    callback.getWidth = getWidth;
    
    // 2 创建CTRunDelegateRef
    CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge void * _Nullable)(self.imageItem));
    
    // 3 设置占位使用的图片属性字符串
    // 参考:https://en.wikipedia.org/wiki/Specials_(Unicode_block)  U+FFFC  OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document.

    unichar objectReplacementChar = 0xFFFC;

    NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1]];
    
    // 4 设置RunDelegate代理
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
    
    CFRelease(runDelegate);
    
    return imagePlaceHolderAttributeString;
}


// MARK: - CTRunDelegateCallbacks 回调方法
static CGFloat getAscent(void *ref) {
    
    float ascent = [(__bridge ImageDrawItem *)ref ascent];
    return ascent;
}
static CGFloat getDescent(void *ref) {
    float descent = [(__bridge ImageDrawItem *)ref descent];
    return descent;
}

static CGFloat getWidth(void *ref) {
    float width = [(__bridge ImageDrawItem *)ref width];
    return width;
}

根据属性字符串和显示范围创建CTFrameRef

- (CTFrameRef)ctFrameWithAttributeString:(NSAttributedString *)attributeString frame:(CGRect)frame {
    // 绘制区域
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, (CGRect){{0, 0}, frame.size});
    
    // 使用NSMutableAttributedString创建CTFrame
    CTFramesetterRef ctFramesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeString);
    CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFramesetter, CFRangeMake(0, attributeString.length), path, NULL);
    
    CFRelease(ctFramesetter);
    CFRelease(path);
    return ctFrame;
}

计算图片所在的位置的代码:

- (void)drawWithFrame:(CTFrameRef) frame context:(CGContextRef)context {
    
    // CTFrameGetLines获取CTFrame内容的行
    NSArray *lines = (NSArray *)CTFrameGetLines(frame);
    // CTFrameGetLineOrigins获取每一行的起始点,保存在lineOrigins数组中
    CGPoint lineOrigins[lines.count];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
    // 遍历行
    for (int i = 0; i < lines.count; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
        
        NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
        // 遍历runs
        for (int j = 0; j < runs.count; j++) {
            CTRunRef run = (__bridge CTRunRef)(runs[j]);
            // 获得属性
            NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
            if (!attributes) {
                continue;
            }
            // 从属性中获取到创建属性字符串使用CFAttributedStringSetAttribute设置的delegate值
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
            if (!delegate) {
                continue;
            }
            
            
            
            // CTRunDelegateGetRefCon方法从delegate中获取使用CTRunDelegateCreate初始时候设置的元数据
            DrawItem *metaData = (DrawItem *)CTRunDelegateGetRefCon(delegate);
            
            
            if (!metaData) {
                continue;
            }
            
            // 找到代理则开始计算图片位置信息
            CGFloat ascent;
            CGFloat desent;
            // 可以直接从metaData获取到图片的宽度和高度信息
            //也可以通过CTRunGetTypographicBounds函数获得
            CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
            
            // CTLineGetOffsetForStringIndex获取CTRun的起始位置
            CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            CGFloat yOffset = lineOrigins[i].y;
         
            // 更新ImageItem对象的位置
            if ([metaData isKindOfClass:[ImageDrawItem class]]) {
                ImageDrawItem *imageItem = metaData;
//                [[UIImage imageNamed:imageItem.imageNamed] drawInRect:CGRectMake(xOffset, yOffset, width, ascent + desent)];
                
                CGContextDrawImage(context, CGRectMake(xOffset, yOffset, width, ascent + desent), [UIImage imageNamed:imageItem.imageNamed].CGImage);
            }
        }
    }
    
    // 使用CTFrame在CGContextRef上下文上绘制
    CTFrameDraw(frame, context);

}

CTFrame会根据CTRunDelegateRef的信息空出一片区域去绘制图片,我们需要通过一些函数,计算出图片的绘制区域,然后调用CGContextDrawImage函数去绘制图片

文字环绕图片

只需要修改path,空出一个区域填图片充就可以了。

内容高亮和事件处理

事件的处理
点击事件的处理基本思路是使用CTFrame对象获取到所有的CTRun对象,遍历CTRun对象,判断CTRun元素是否可以点击

  1. 给NSMutableAttributedString设置自定义属性,表示这个NSMutableAttributedString对应的CTRun是可以点击的
  2. CTFrame遍历CTRun,取出在上一步设置的特殊属性,计算CTRun最终渲染显示的位置,记录可点击元素的位置

首先创建模型,存储相关信息

UIKIT_EXTERN NSString * ActionAndHilightedItemKey;

@interface ActionAndHilightedItem : NSObject
@property (nonatomic , strong) void(^action)(void);

@property (nonatomic , strong) UIColor *highlightedBackgroundColor;

@property (nonatomic , strong) UIColor *highlightedColor;

@property (nonatomic , strong) NSMutableArray *frames;

@end

NSMutableAttributedString设置自定义属性

- (void) appendAttributedString:(NSAttributedString *)str action:(void(^)(void))action highlightedTextColor:(UIColor *)highlightedTextColor highlightedBackgroundColor:(UIColor *)highlightedBackgroundColor{
    
    NSMutableAttributedString *attriStr = [[NSMutableAttributedString alloc] initWithAttributedString:str];
    
    ActionAndHilightedItem *item = [[ActionAndHilightedItem alloc] init];
    
    item.action = action;
    item.highlightedColor = highlightedTextColor;
    item.highlightedBackgroundColor = highlightedBackgroundColor;
    
    [attriStr addAttributes:@{ActionAndHilightedItemKey:item} range:NSMakeRange(0, str.length)];
    
    [self.string appendAttributedString:attriStr];
}

定义两个属性来存储相关的配置

@property (nonatomic , strong) NSMutableSet <ActionAndHilightedItem *>*actionAndHighlightedItems;

@property (nonatomic , strong) ActionAndHilightedItem *currentItem;

绘制代码

- (void)drawRect:(CGRect)rect{
    [super drawRect:rect];
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1, -1);
    
    if (self.currentItem) {
        [self.string enumerateAttribute:ActionAndHilightedItemKey inRange:NSMakeRange(0, self.string.length) options:0 usingBlock:^(id  _Nullable value, NSRange range, BOOL * _Nonnull stop) {
            
            if (value == self.currentItem) {
             
                if (self.currentItem.highlightedColor != nil) {
                    [self.string addAttributes:@{NSForegroundColorAttributeName:self.currentItem.highlightedColor} range:range];
                }
                
                if (self.currentItem.highlightedBackgroundColor) {
                    [self.string addAttributes:@{NSBackgroundColorAttributeName:self.currentItem.highlightedBackgroundColor} range:range];
                }
                *stop = YES;
            }
        }];
    }
    else{
        
        [self.string addAttributes:@{NSForegroundColorAttributeName:[UIColor blackColor]} range:NSMakeRange(0, self.string.length)];
        [self.string addAttributes:@{NSBackgroundColorAttributeName:[UIColor clearColor]} range:NSMakeRange(0, self.string.length)];
    
    }
    CTFrameRef frame = [DrawTool ctFrameWithAttributeString:self.string.copy frame:rect];
    [self drawWithFrame:frame rect:rect context:context];
}

- (void)drawWithFrame:(CTFrameRef)frame rect:(CGRect)rect context:(CGContextRef)context {
    
    [self.actionAndHighlightedItems removeAllObjects];
    
    // CTFrameGetLines获取但CTFrame内容的行数
    NSArray *lines = (NSArray *)CTFrameGetLines(frame);
    // CTFrameGetLineOrigins获取每一行的起始点,保存在lineOrigins数组中
    CGPoint lineOrigins[lines.count];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
    for (int i = 0; i < lines.count; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
    
        NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
        for (int j = 0; j < runs.count; j++) {
            CTRunRef run = (__bridge CTRunRef)(runs[j]);
            
            NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
            if (!attributes) {
                continue;
            }
            
            // 获取附加的数据->设置链接、图片等元素的点击效果的位置
            ActionAndHilightedItem *extraData = (ActionAndHilightedItem *)[attributes valueForKey:ActionAndHilightedItemKey];
            if (extraData) {
                // 获取CTRun的信息
                CGFloat ascent;
                CGFloat desent;
                // 可以直接从metaData获取到图片的宽度和高度信息
                CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
                CGFloat height = ascent + desent;
                
                // CTLineGetOffsetForStringIndex获取CTRun的起始位置
                CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                
                // lineOrigins[i].y;基于baseline的?
//                CGFloat yOffset = lineOrigins[i].y;
                CGFloat yOffset = rect.size.height - lineOrigins[i].y - ascent;
                
                
                                    // 由于CoreText和UIKit坐标系不同所以要做个对应转换
                    // CGRect ctClickableFrame = CGRectMake(xOffset, yOffset, width, height);
                    // 将CoreText坐标转换为UIKit坐标
                CGRect uiKitClickableFrame = CGRectMake(xOffset, yOffset, width, height);
                [extraData.frames addObject:@(uiKitClickableFrame)];
                [self.actionAndHighlightedItems addObject:extraData];
            }
        }
    }
    
    CTFrameDraw(frame, context);
    
}

点击效果处理
数据处理好了,点时效果是判断点击位置是否存在相应配置,如果有则重新绘制
点击事件的监听

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
   
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    
    NSArray <ActionAndHilightedItem *> *enumerator = [self.actionAndHighlightedItems objectEnumerator].allObjects;
        
        ActionAndHilightedItem *item = nil;
        
    for (int i = 0; i<enumerator.count; i++) {
        
        item = enumerator[i];
            for (NSInteger i = 0; i<item.frames.count; i++) {
                CGRect frame = [item.frames[i] CGRectValue];
                if (CGRectContainsPoint(frame, point)) {
                    self.currentItem = item;
                    
                    [self setNeedsDisplay];
                    return;
                }
            }
        }
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    
    if (self.currentItem == nil) {
        return;;
    }
    
    for (NSInteger i = 0; i<self.currentItem.frames.count; i++) {
        CGRect frame = [self.currentItem.frames[i] CGRectValue];
        if (CGRectContainsPoint(frame, point)) {
            
            if(self.currentItem.action){
                self.currentItem.action();
            }
            
            break;
        }
    }
    
    self.currentItem = nil;
    [self setNeedsDisplay];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.currentItem = nil;
    [self setNeedsDisplay];

}
文字行数限制和显示更多

文字行数限制

  1. 判断是否有行数限制,没有使用默认绘制(CTFrameDraw)即可,
  2. 有行数限制且不是最后一行直接绘制(使用CTLineDraw,并且需要使用CGContextSetTextPosition方法设置绘制文本的位置)
  3. 判断最后一行的显示是否会超出,超出则把最后一行的内容截取,拼接“...”在被截取的原始内容之后
    使用CTLineCreateTruncatedLine创建最后一行显示的内容,返回CTLine对象
    如果设置了截断标识字符串点击事件,需要把位置信息进行保存,用于后面的事件处理

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    // 坐标系调整
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, rect.size.height));

    // 绘制的内容属性字符串
    NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:18],
                                  NSForegroundColorAttributeName: [UIColor blueColor]
                                  };
    
    
    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:@"你的回话凌乱着 在这个时刻\n\
我想起喷泉旁的白鸽 甜蜜散落了\n\
情绪莫名的拉扯 我还爱你呢\n\
而你断断续续唱着歌 假装没事了\n\
时间过了 走了 爱情面临选择\n\
你冷了 倦了 我哭了\n\
离开时的不快乐 你用卡片手写着\n\
有些爱只给到这 真的痛了" attributes:attributes];

    CTFrameRef frame = [DrawTool ctFrameWithAttributeString:attrStr frame:rect];
    
    
    if (self.showAll) {
        CTFrameDraw(frame, context);
    }
    else{
        NSArray *lines = (NSArray *)CTFrameGetLines(frame);

        // 创建结构体数组,保存行的起始位置
        CGPoint lineOrigins[3];
        // 获取每一行的起点位置
        CTFrameGetLineOrigins(frame, CFRangeMake(0, 3), lineOrigins);
        for (int lineIndex = 0; lineIndex < 3; lineIndex ++) {
            // 获得行
            CTLineRef line = (__bridge CTLineRef)(lines[lineIndex]);
            // 获取行显示的文本的range
            CFRange range = CTLineGetStringRange(line);
            // 判断最后一行是否能够显示完文字
            if (lineIndex == 2
                       && range.location + range.length < attrStr.length) {
                       // 创建截断字符串
                
                self.actionModel = [[ActionAndHilightedItem alloc] init];
                __weak NumberOfLinesAndShowMoreView *weaSelf = self;
                self.actionModel.action = ^(){
                    
                    __strong NumberOfLinesAndShowMoreView *self = weaSelf;
                    self.showAll = YES;
                    [self setNeedsDisplay];
                };
                
                
                NSAttributedString *tokenString = [[NSMutableAttributedString alloc] initWithString:@"查看全部" attributes:@{ActionAndHilightedItemKey:self.actionModel ,NSForegroundColorAttributeName:[UIColor blackColor],NSFontAttributeName:[UIFont systemFontOfSize:20]}];
                                
                // 创建 截断的 CTLine
                CTLineRef truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)tokenString);
                   
                // 截断的类型
                CTLineTruncationType truncationType = kCTLineTruncationEnd;
                
                double lineW = CTLineGetTypographicBounds(line,NULL,NULL,NULL);
                
                CTLineRef lastLine = CTLineCreateTruncatedLine(line, lineW-0.1 , truncationType, truncationTokenLine);
                                
                                // 添加truncation的位置信息
                NSArray *runs = (NSArray *)CTLineGetGlyphRuns(lastLine);
                
                for (NSInteger runIndex = 0; runIndex<runs.count; runIndex ++) {
                    CTRunRef run = (__bridge CTRunRef)(runs[runIndex]);
                    
                    NSDictionary *dict = CTRunGetAttributes(run);
                    
                    if ([dict objectForKey:ActionAndHilightedItemKey] != nil) {
                        
                        ActionAndHilightedItem *item = dict[ActionAndHilightedItemKey];
                        
                        
                        // 获取CTRun的信息
                        CGFloat ascent;
                        CGFloat desent;
                        // 可以直接从metaData获取到图片的宽度和高度信息
                        CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
                        CGFloat height = ascent + desent;
                        
                        // CTLineGetOffsetForStringIndex获取CTRun的起始位置
                        CGFloat xOffset = lineOrigins[lineIndex].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                        
                        // lineOrigins[i].y;基于baseline的?
        //                CGFloat yOffset = lineOrigins[i].y;
                        CGFloat yOffset = rect.size.height - lineOrigins[lineIndex].y - ascent;
                        
                        
                                            // 由于CoreText和UIKit坐标系不同所以要做个对应转换
                            // CGRect ctClickableFrame = CGRectMake(xOffset, yOffset, width, height);
                            // 将CoreText坐标转换为UIKit坐标
                        CGRect uiKitClickableFrame = CGRectMake(xOffset, yOffset, width, height);
                        [item.frames addObject:@(uiKitClickableFrame)];
                    }
                    
                }
                CFRelease(truncationTokenLine);
                CGContextSetTextPosition(context, lineOrigins[lineIndex].x, lineOrigins[lineIndex].y);
                
                CTLineDraw(lastLine, context);
                
                CGContextSetTextPosition(context, lineOrigins[lineIndex].x, lineOrigins[lineIndex].y - 30);

                CTLineDraw(line, context);
                                
                            
            } else {
                       
                CGContextSetTextPosition(context, lineOrigins[lineIndex].x, lineOrigins[lineIndex].y);

                CTLineDraw(line, context);
                           
            }
        }
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.isTouchActionModel = NO;
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    
    ActionAndHilightedItem *item = self.actionModel;
        
        
    for (NSInteger i = 0; i<item.frames.count; i++) {
        CGRect frame = [item.frames[i] CGRectValue];
        if (CGRectContainsPoint(frame, point)) {
            self.isTouchActionModel = YES;
            return;
        }
    }
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    
    if (self.isTouchActionModel == NO) {
        return;;
    }
    
    NSLog(@"开始匹配");
    
    for (NSInteger i = 0; i<self.actionModel.frames.count; i++) {
        CGRect frame = [self.actionModel.frames[i] CGRectValue];
        
        
        
        if (CGRectContainsPoint(frame, point)) {
            NSLog(@"匹配到了");
            
            if(self.actionModel.action){
                self.actionModel.action();
            }
            
            self.isTouchActionModel = NO;
            return;
        }
    }
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.isTouchActionModel = NO;
}
文字排版样式和效果

排版效果有以下几种

  • 字体
  • 颜色
  • 阴影
  • 行间距
  • 对齐方式
  • 段间距
    本质上都是设置NSMutableAttributedString的属性
  • 行间距对其段间距行高是段落属性,使用kCTParagraphStyleAttributeNamekey设置对应的属性
  • 阴影使用需要使用CoreGraphics的API CGContextSetShadowWithColor 进行设置的
  • 字体使用kCTFontAttributeName进行设置;颜色使用kCTForegroundColorAttributeName进行设置

kCTParagraphStyleAttributeName

/**
 设置排版样式
 */
- (void)setStyleToAttributeString:(NSMutableAttributedString *)attributeString {
    CTParagraphStyleSetting settings[] =
    {
        {kCTParagraphStyleSpecifierAlignment,sizeof(self.textAlignment),&_textAlignment},
        {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(self.lineSpacing),&_lineSpacing},
        {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(self.lineSpacing),&_lineSpacing},
        {kCTParagraphStyleSpecifierParagraphSpacing,sizeof(self.paragraphSpacing),&_paragraphSpacing},
    };
    CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));
    [attributeString addAttribute:(id)kCTParagraphStyleAttributeName
                       value:(__bridge id)paragraphStyle
                       range:NSMakeRange(0, [attributeString length])];
    CFRelease(paragraphStyle);
}

CGContextSetShadowWithColor

if (shadowColor == nil
        || CGSizeEqualToSize(shadowOffset, CGSizeZero)) {
        return;
    }
    CGContextSetShadowWithColor(context,shadowOffset, shadowAlpha, shadowColor.CGColor);
内容大小计算和自动布局

常用有三种方式布局的效果

  • 手动布局手动计算高度
  • 自动布局自动计算高度
  • 自动布局中限制了内容高度

手动布局手动计算高度
计算内容大小需要重写UIView的方法sizeThatFits,返回一个CGSize。

  • CTLineGetStringRange方法获取最后一行CTLineRef(如果设置的行数限制需要使用,否则不用这个步骤)的内容显示的范围,返回一个CFRange对象
  • CTFramesetterSuggestFrameSizeWithConstraints方法计算指定范围内容的大小,第二个参数是CFRange类型,在设置行数限制的情况传递上面获取到的CFRange对象,没有行数限制的情况直接设置一个空的CFRange对象(CFRangeMake(0, 0))

- (CGSize)sizeThatFits:(CGSize)size {
    NSAttributedString *drawString = self.data.attributeStringToDraw;
    if (drawString == nil) {
        return CGSizeZero;
    }
    CFAttributedStringRef attributedStringRef = (__bridge CFAttributedStringRef)drawString;
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedStringRef);

    CFRange range = CFRangeMake(0, 0);
    if (_numberOfLines > 0 && framesetter) {
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
        CFArrayRef lines = CTFrameGetLines(frame);
        
        if (nil != lines && CFArrayGetCount(lines) > 0) {
            NSInteger lastVisibleLineIndex = MIN(_numberOfLines, CFArrayGetCount(lines)) - 1;
            CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
            
            CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
            range = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
        }
        CFRelease(frame);
        CFRelease(path);
    }
    
    CFRange fitCFRange = CFRangeMake(0, 0);
    CGSize newSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, range, NULL, size, &fitCFRange);
    if (framesetter) {
        CFRelease(framesetter);
    }
    
    return newSize;
}

自动布局
自动布局会调用intrinsicContentSize方法获取内容的大小,所以重写这个方法,这个方法调用sizeThatFits方法获取到内容的大小,然后返回即可,

// 计算显示自己需要多大的size
- (CGSize)intrinsicContentSize {
    return [self sizeThatFits:CGSizeMake(self.bounds.size.width, MAXFLOAT)];
}
添加自定义View和设置对齐方式

添加View其实和添加图片的处理方式很类似,只不过添加图片我们是使用CG绘图的方式把图片绘制在View上,而添加View是使用UIkit的方法addSubview把View添加到View的层级上
添加view
首先定义一个添加View的方法,在该方法中主要是进行数据模型的保存以及生产特殊的占位属性字符串,然后添加属性字符串的RunDelegate

- (void)addView:(UIView *)view size:(CGSize)size align:(YTAttachmentAlignType)align clickActionHandler:(ClickActionHandler)clickActionHandler {
    YTAttachmentItem *imageItem = [YTAttachmentItem new];
    [self updateAttachment:imageItem withFont:self.font];
    imageItem.align = align;
    imageItem.attachment = view;
    imageItem.type = YTAttachmentTypeView;
    imageItem.size = size;
    imageItem.clickActionHandler = clickActionHandler;
    [self.attachments addObject:imageItem];
    NSAttributedString *imageAttributeString = [self attachmentAttributeStringWithAttachmentItem:imageItem size:size];
    [self.attributeString appendAttributedString:imageAttributeString];
}

- (NSAttributedString *)attachmentAttributeStringWithAttachmentItem:(YTAttachmentItem *)attachmentItem size:(CGSize)size {
    // 创建CTRunDelegateCallbacks
    CTRunDelegateCallbacks callback;
    memset(&callback, 0, sizeof(CTRunDelegateCallbacks));
    callback.getAscent = getAscent;
    callback.getDescent = getDescent;
    callback.getWidth = getWidth;
    
    // 创建CTRunDelegateRef
//    NSDictionary *metaData = @{YTRunMetaData: attachmentItem};
    CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge void * _Nullable)(attachmentItem));
    
    // 设置占位使用的图片属性字符串
    // 参考:https://en.wikipedia.org/wiki/Specials_(Unicode_block)  U+FFFC  OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document.
    unichar objectReplacementChar = 0xFFFC;
    NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1] attributes:[self defaultTextAttributes]];
    
    // 设置RunDelegate代理
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
    
    // 设置附加数据,设置点击效果
    NSDictionary *extraData = @{YTExtraDataAttributeTypeKey: attachmentItem.type == YTAttachmentTypeImage ? @(YTDataTypeImage) : @(YTDataTypeView),
                                YTExtraDataAttributeDataKey: attachmentItem,
                                };
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), (CFStringRef)YTExtraDataAttributeName, (__bridge CFTypeRef)(extraData));
    
    CFRelease(runDelegate);
    return imagePlaceHolderAttributeString;
}

// MARK: - CTRunDelegateCallbacks 回调方法
static CGFloat getAscent(void *ref) {
    YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
    if (attachmentItem.align == YTAttachmentAlignTypeTop) {
        return attachmentItem.ascent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeBottom) {
        return attachmentItem.size.height - attachmentItem.descent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeCenter) {
        return attachmentItem.ascent - ((attachmentItem.descent + attachmentItem.ascent) - attachmentItem.size.height) / 2;
    }
    return attachmentItem.size.height;
}

static CGFloat getDescent(void *ref) {
    YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
    if (attachmentItem.align == YTAttachmentAlignTypeTop) {
        return attachmentItem.size.height - attachmentItem.ascent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeBottom) {
        return attachmentItem.descent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeCenter) {
        return attachmentItem.size.height - attachmentItem.ascent + ((attachmentItem.descent + attachmentItem.ascent) - attachmentItem.size.height) / 2;
    }
    return 0;
}

static CGFloat getWidth(void *ref) {
    YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
    return attachmentItem.size.width;
}

计算添加的View在父View中的位置,可以直接复用添加image的方法

对齐方式

image.png

bounding box(边界框),这是一个假想的框子,它尽可能紧密的装入字形。
baseline(基线),一条假想的线,一行上的字形都以此线作为上下位置的参考,一般在文本的3/4处,在这条线的左侧存在一个点叫做基线的原点。
ascent(上行高度),从原点到字体中最高(这里的高深都是以基线为参照线的)的字形的顶部的距离,ascent是一个正值。
descent(下行高度),从原点到字体中最深的字形底部的距离,descent是一个负值(比如一个字体原点到最深的字形的底部的距离为2,那么descent就为-2)。

基于以下数据分析对齐方式

Font.fontAscent = 33.75.   
Font.fontDescent = 27.04. 
LineHeight = Font.fontAscent + Font.fontDescent = 60.8. 

顶部对齐
需要设置ascent值为文字内容的ascent,descent值为attachmen的高度减去ascent,当内容的高度为40

  • ascent= Font.fontAscent = 33.75.
  • descent = 40 - ascent = 6.25.
ascent = 33.75. 
descent = 6.25. 
height = ascent + descent = 40. 
baseline = 33.75. 
image.png

底部对齐
需要设置descent值为文字内容的descent,ascent值为attachmen的高度减去ascent,

  • descent= Font.fontDescent = 27.04.
  • ascent = 40 - descent = 12.95.
ascent = 12.95. 
descent = 27.04. 
height = ascent + descent = 40.
image.png
居中对齐
image.png
先计算ascent值,ascent值为文字内容的ascent减去顶部的那一段差值,(如下图标准中的值为21处的高度),然后descent值为attachmen的高度减去ascent,如下图所示(图片上的标注是2x,并且数值因为是手动使用工具标注,会有一些细微的偏差),内容的高度为40,所以有:
  • ascent = Font.fontAscent - (LineHeight - 40)/2 = 23.35.
  • descent = 40 - ascent = 16.64.
    ascent = 23.35.
    descent = 16.64.
    height = ascent + descent = 40.

参考链接
https://my.oschina.net/FEEDFACF/blog/1845922

相关文章

网友评论

      本文标题:iOS 文本相关-CoreText

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