介紹 CoreText 簡(jiǎn)單應(yīng)用,主要包括文本節(jié)選,可點(diǎn)鏈接,圖文混排等內(nèi)容。
CoreText
CoreText 是用于處理文字和字體的底層技術(shù)。它直接和 Core Graphics 交互。
CoreText 對(duì)象能直接獲取文本的寬高信息,占用內(nèi)存少,異步繪制等特點(diǎn)。在引起 UITableView 卡頓常見(jiàn)的原因 Cell 層級(jí)過(guò)多,離屏渲染,頻繁計(jì)算 Cell 高度等耗時(shí)操作。這個(gè)時(shí)候 CoreText 就派上用場(chǎng)了,減少層級(jí),CoreText 可以直接將文字和圖片直接繪制在 Layer 上,并且支持異步繪制大大節(jié)約主線程資源。用來(lái)做圖文混排的 UITableView 的優(yōu)化,效果很明顯。
基礎(chǔ)概念
Font & Character & Glyphs
Font 在計(jì)算機(jī)意義上字體表示的是同一個(gè)大小,同一樣式字形的集合
Character 字符表示信息本身,字形是它的圖形表示形式,字符一般指某種編碼,比如 Unicode 編碼就是其中一種。字符和字形不是一一對(duì)應(yīng)關(guān)系,同一個(gè) Character 不同 Font 會(huì)生成不同的 Glyphs
Glyphs 字形常見(jiàn)參數(shù)
- Baseline : 參照線,是一條橫線,一般為此為基礎(chǔ)進(jìn)行字體的渲染
- Leading : 行與行之間的間距
- Kerning : 字與字之間的間距
- Origin : 基線上最左側(cè)的點(diǎn)
- Ascent : 一個(gè)字形最高點(diǎn)到基線的距離
- Decent : 一個(gè)自行最低處到基線的距離,所以一個(gè)字符的高度是 ascent + decent 。當(dāng)一行內(nèi)有不同字體的文字時(shí)候,取最大值 max(ascent + decent)。
- Line Height : max(ascent + decent) + Leading
富文本NSAttributedString
iOS 中用于描述富文本的類,它比 String 多了很多描述字體的屬性,.font,.underlineColor,.foregroundColor 等,而且可以設(shè)定屬性對(duì)應(yīng)的區(qū)域 NSRange
let text: NSMutableAttributedString = NSMutableAttributedString(string: "test")
let attributes: [NSAttributedStringKey: Any] = [.font: UIFont.systemFontSize, .foregroundColor: UIColor.black, .underlineColor: UIColor.blue]
text.addAttributes(attributes, range: NSMakeRange(0, 1))
在繪制過(guò)程中,其中 CTFramesetter 是由 CFAttributedString(NSAttributedString) 初始化而來(lái),通過(guò)傳入 CGPath 生成相應(yīng)的 CTFrame 最后渲染到屏幕是 CTFrame
let frameSetter: CTFramesetter = CTFramesetterCreateWithAttributedString(text)
let path = UIBezierPath(rect: CGRect())
let frame: CTFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path.cgPath, nil)
一個(gè) CTFrame 由一個(gè)或者多個(gè) CTLine 組成,一個(gè) CTLine 由一個(gè)或者多個(gè) CTRun 組成。一個(gè) CTRun 是由相同屬性的字符組成。
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() { return }
...
CTFrameDraw(ctFrame, context)
...
let lines = CTFrameGetLines(frame) as! Array
...
let runs = CTLineGetGlyphRuns(lines[0] as! CTLine)
...
}
產(chǎn)品需求是做一個(gè)類似知乎的問(wèn)答系統(tǒng),支持圖文,標(biāo)簽,鏈接,短視頻等基本元素。本文主要介紹基于 CoreText 圖文排版一些簡(jiǎn)單實(shí)踐應(yīng)用。
文本
直接看代碼吧,簡(jiǎn)單輸出一段文字。
// BKCoreTextConfig.swift
// 文本配置信息
struct BKCoreTextConfig {
let width : CGFloat // 文本最大寬度
let fontName : String
let fontSize : CGFloat
let lineSpace : CGFloat // 行間距
let textColor : UIColor
init(width: CGFloat, fontName: String, fontSize: CGFloat,
lineSpace: CGFloat, textColor: UIColor) {
self.width = width
self.fontName = fontName
self.fontSize = fontSize
self.lineSpace = lineSpace
self.textColor = textColor
}
}
// BKCoreTextData.swift
// 繪制信息內(nèi)容
struct BKCoreTextData {
let ctFrame : CTFrame
let size : CGSize
init(ctFrame: CTFrame, contentSize: CGSize) {
self.ctFrame = ctFrame
self.size = contentSize
}
}
// BKCoreTextParser.swift
// 解析
static func attributes(with config: BKCoreTextConfig) -> NSDictionary {
// 字體大小
let font = CTFontCreateWithName(config.fontName as CFString, config.fontSize, nil)
//設(shè)置行間距
var lineSpace = config.lineSpace
let settings: [CTParagraphStyleSetting] =
[CTParagraphStyleSetting(spec: CTParagraphStyleSpecifier.lineSpacingAdjustment, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace),
CTParagraphStyleSetting(spec: CTParagraphStyleSpecifier.maximumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace),
CTParagraphStyleSetting(spec: CTParagraphStyleSpecifier.minimumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace)]
let paragaraph = CTParagraphStyleCreate(settings, 3)
//設(shè)置字體顏色
let textColor = config.textColor
let dict = NSMutableDictionary()
dict.setObject(font, forKey: kCTFontAttributeName as! NSCopying)
dict.setObject(paragaraph, forKey: kCTParagraphStyleAttributeName as! NSCopying)
dict.setObject(textColor.cgColor, forKey: kCTForegroundColorAttributeName as! NSCopying)
return dict
}
static func createFrame(frameSetter: CTFramesetter, config: BKCoreTextConfig, height: CGFloat) -> CTFrame {
let path = CGMutablePath()
path.addRect(ccr(x: 0, y: 0, width: config.width, height: height))
return CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
}
static func parse(content: NSAttributedString, config: BKCoreTextConfig) -> BKCoreTextData {
let frameSetter = CTFramesetterCreateWithAttributedString(content)
let restrictSize = CGSize(width: config.width, height: CGFloat(MAXFLOAT))
let coretextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter,
CFRangeMake(0, 0), nil, restrictSize, nil)
let height = coretextSize.height
let frame = self.createFrame(frameSetter: frameSetter, config: config, height: height)
retutn BKCoreTextData.init(ctFrame: frame, contentSize: coretextSize)
}
static func handleText(text: String, config: BKCoreTextConfig) -> BKCoreTextData {
/*
let text = "裘德洛論顏值的話,絕對(duì)可以稱得上帥的人神共憤,海洋般藍(lán)綠交織的雙眼,優(yōu)雅俊美,隨隨便便一個(gè)動(dòng)作都能俘獲萬(wàn)千少女的心。而且人家不止有顏還多才多藝,小小年紀(jì)開(kāi)始就在音樂(lè)劇團(tuán)表演,氣質(zhì)啊才華啊什么的,完美的讓人嫉妒。美圖奉上↓"
let config = BKCoreTextConfig(width: kScreenWidth - 30, fontName: "PingFangSC-Regular", fontSize: 12, textColor: UIColor(rgb: 0x9E9E9E))
*/
let attributes = self.attributes(with: config) as? [NSAttributedStringKey : Any]
let attributedString = NSMutableAttributedString(string: content, attributes: attributes)
return parse(content: attributedString, config: config)
}
上面就是給定給一個(gè)文本和文本一些配置信息得到一個(gè) frame 現(xiàn)在可以直接繪制出一段文本
// BKCoreTextView.swift
var data : BKCoreTextData? {
didSet { setNeedsDisplay() }
}
....
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext(), let info = data else { return }
/// !!! 坐標(biāo)轉(zhuǎn)換
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: bounds.size.height)
context.scaleBy(x: 1, y: -1)
CTFrameDraw(info.ctFrame, context)
}
CoreText 坐標(biāo)系是以左下角為坐標(biāo)原點(diǎn),UIKit是以左上角為坐標(biāo)原點(diǎn),使用 Core Graphics 需要做坐標(biāo)的轉(zhuǎn)換,不然看到的內(nèi)容是倒過(guò)來(lái)的。
文本節(jié)選
以上只是簡(jiǎn)單的繪制了一段文本,但是呢,產(chǎn)品有需求要限制文本的行數(shù),超過(guò)的用 ...
來(lái)表示更多。類似于微信朋友圈,內(nèi)容過(guò)多會(huì)有 收起
全部
的按鈕,功能是相類似的。
如果用 UILabel 設(shè)置寬高,UILabel 會(huì)自動(dòng)幫我們處理 ...
。但是 CoreText 需要開(kāi)發(fā)組手動(dòng)處理,這里主要問(wèn)題就是要找到最后一行合適的位置放置 ...
思路:
文本通過(guò)相關(guān)配置文件轉(zhuǎn)換為 NSAttributedString 格式
計(jì)算文本的行數(shù) Count
文本行數(shù) Count 小于指定行數(shù) numberOfLines,返回?zé)o需處理
文本行數(shù)大于指定行數(shù),截取最后一行處理
最后一行顯示寬度小于 Config.width, 直接行尾添加
...
最后一行顯示寬度大于等于 Config.width ,需對(duì)最后一行做 replace 操作
難點(diǎn): 如何獲取最后一行顯示寬度
public func CTLineGetOffsetForStringIndex(_ line: CTLine, _ charIndex: CFIndex, _ secondaryOffset: UnsafeMutablePointer<CGFloat>?) -> CGFloat
函數(shù) CTLineGetOffsetForStringIndex 是獲取一行文字中指定 charIndex 字符相對(duì)原點(diǎn)的偏移量,返回值與 secondaryOffset 同為一個(gè)值。如果 charIndex 超出一行的字符長(zhǎng)度則返回最大長(zhǎng)度結(jié)束位置的偏移量。因此想求一行字符所占的像素長(zhǎng)度時(shí),就可以使用此函數(shù),將 charIndex 設(shè)置為大于字符長(zhǎng)度即可這里設(shè)置了 100
但是感受算出來(lái)的長(zhǎng)度還是有一丟丟誤差。
static func handleText(text: String, config: BKCoreTextConfig, numberofLines: Int) -> BKCoreTextData {
let attributes = self.attributes(with: config) as? [NSAttributedStringKey : Any]
let attributedString = NSMutableAttributedString(string: content, attributes: attributes)
let currCoreData = self.parse(content: attributedString, config: config)
let lines = CTFrameGetLines(currCoreData.ctFrame) as Array
let count = lines.count
guard numberofLines > 0 else { return currCoreData }
guard count > 0 && count > numberofLines else { return currCoreData }
let num = min(numberofLines, count)
let line = lines[num-1]
let range = CTLineGetStringRange(line as! CTLine)
let position = range.location + range.length
let tmpAttrString = attr.attributedSubstring(from: NSMakeRange(0, position))
var newContent = NSAttributedString()
var offset: CGFloat = 0
CTLineGetOffsetForStringIndex(line as! CTLine,100,&offset)
let length = offset > (config.width - 10) ? range.length - 3 : range.length
let lastLine: NSMutableAttributedString = tmpAttrString.attributedSubstring(from: NSMakeRange(range.location, length)) as! NSMutableAttributedString
/// !!! 去除最后一行的 \n
var str = (lastLine.mutableString.mutableCopy() as! String).replacingOccurrences(of: "\n", with: "")
str.append("...")
let tmp = tmpAttrString.attributedSubstring(from: NSMakeRange(0, range.location))
let newAttr: NSAttributedString = tmp.appending(NSAttributedString.init(string: str))
let attributes = self.attributes(with: config) as? [NSAttributedStringKey : Any]
newContent = NSMutableAttributedString(string: newAttr.string, attributes: attributes)
return self.parseContent(content: newContent, config: config)
}
可點(diǎn)鏈接
可點(diǎn)鏈接也是很常見(jiàn)的,比如 點(diǎn)我跳轉(zhuǎn)
后臺(tái)給的 JSON 字符串可能直接原生態(tài)甩過(guò)來(lái)
print(content)
"嘎嘎嘎嘎哈哈哈哈哈https://wapbaike.baidu.com/item/%e4%b8%9c%e4%ba%ac%e5%9b%bd%e9%99%85%e7%94%b5%e5%bd%b1%e8%8a%82/187783?fr=aladdin"
********思路:********
掃描文本,找出鏈接地址的字符串 results
文本通過(guò)相關(guān)配置文件 Config 轉(zhuǎn)換為 attributedString
超鏈接文本通過(guò)配置文件 Config 轉(zhuǎn)換為 linkAttributedString
遍歷 results 用 linkAttributedString 替換 attributedString 中鏈接地址
記錄 linkAttributedString 的 range,url 得到 coreTextLinkDatas
// BKCoreTextData.swift
/// 可點(diǎn)擊鏈接
struct BKCoreTextLinkData {
let title : String
let url : String
let range : NSRange
}
struct BKCoreTextData {
...
var linkData : [BKCoreTextLinkData]?
...
}
static func handleText(text: String, config: BKCoreTextConfig, numberofLines: Int) -> BKCoreTextData {
return self.handleLinkAttribute(content: content, config: config) { (attributedString, linkDatas) in
// 同上
....
let newCoreData = self.parseContent(content: attributedString, config: config)
if let links = linkDatas, links.count > 0 {
newCoreData.linkData = links
}
return newCoreData
}
static func handleLinkAttribute(content: String, config: BKCoreTextConfig, completed: @escaping
( _ result : NSAttributedString, _ linkDatas : [BKCoreTextLinkData]?) -> BKCoreTextData) -> BKCoreTextData {
let dataDetector = try? NSDataDetector(types: NSTextCheckingTypes(NSTextCheckingResult.CheckingType.link.rawValue))
let results = dataDetector?.matches(in: content, options: NSRegularExpression.MatchingOptions.reportProgress, range: NSMakeRange(0, content.length))
let attributes = self.attributes(with: config) as? [NSAttributedStringKey : Any]
let attributedString = NSMutableAttributedString(string: content, attributes: attributes)
let linkAttributedString = IconCodes.attributedString(code: .link, size: config.link.fontSize, color: config.link.textColor).appending(NSAttributedString(string: " 網(wǎng)頁(yè)鏈接", font: UIFont(name: config.link.fontName, size: config.link.fontSize)!, color: .app_light))
var tmpLinkDatas = [BKCoreTextLinkData]()
if let results = results {
results.reversed().forEach({ (result) in
if result.resultType == .link, let url = URL(string: (content as NSString).substring(with: result.range)) {
linkAttributedString.addAttributes([.link: url], range: NSMakeRange(0, linkAttributedString.length))
attributedString.replaceCharacters(in: result.range, with: linkAttributedString)
let data = BKCoreTextLinkData.init(title: linkAttributedString.string, url: url.absoluteString, range: NSMakeRange(result.range.location, linkAttributedString.length))
tmpLinkDatas.append(data)
}
})
}
return completed(attributedString, tmpLinkDatas)
}
}
一個(gè)文本中可能有多個(gè)鏈接,需要識(shí)別鏈接,以及記下每個(gè)鏈接的 Range
效果有了,點(diǎn)擊事件要怎么響應(yīng),這個(gè)時(shí)候就用到了上述記錄的鏈接對(duì)應(yīng)的 Range
遍歷 coreTextLinkDatas ,找到在 range 中包含獲取點(diǎn)擊位置 point 的coreTextLinkData 拿到 url 地址
// BKCoreTextView.swift
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = (touches as NSSet).anyObject() as! UITouch
let point = touch.location(in: self)
guard let frame = data?.ctFrame else { return }
let lines = CTFrameGetLines(frame) as Array
let count = lines.count
var origins = [CGPoint].init(repeating: CGPoint(0, 0), count: count)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
var transform = CGAffineTransform.init(translationX: 0, y: bounds.height)
transform = transform.scaledBy(x: 1, y: -1)
guard let links = data?.linkData, links.count > 0 else {
print("沒(méi)有可點(diǎn)擊鏈接")
return
}
for (index, line) in lines.enumerated() {
let origin = origins[index]
let lineRect = getLineBound(line: line as! CTLine, point: origin)
let rect = lineRect.applying(transform)
if rect.contains(point) == true {
let relativePoint = CGPoint(point.x - rect.minX, point.y - rect.minY)
let idx = CTLineGetStringIndexForPosition(line as! CTLine, relativePoint)
if let link = foundLinkData(at: idx), let url = URL.init(string: link.url) {
print("oh! 點(diǎn)到了。\(url)")
return
} else {
print("不在點(diǎn)擊鏈接范圍")
}
}
}
}
func getLineBound(line: CTLine, point: CGPoint) -> CGRect {
var ascent: CGFloat = 0
var descent: CGFloat = 0
var leading: CGFloat = 0
let width: CGFloat = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, &leading))
let height: CGFloat = ascent + descent
return ccr(point.x, point.y - descent, width, height)
}
func foundLinkData(at index: Int) -> BKCoreTextLinkData? {
var link : BKCoreTextLinkData?
data?.linkData?.forEach( {
if NSLocationInRange(index, $0.range ) == true {
link = $0
}
})
return link
}
這只是比較簡(jiǎn)單的鏈接樣式,可以給它添加下劃線,按壓態(tài)等等,設(shè)置它的 NSAttributedString 樣式就可以了。
圖片混排
就是圖片和文字混合排版,如果圖片比較多文字少不建議用 Core Text。
Core Text 是一個(gè)文本處理框架,不能直接繪制圖片,但是它可以給圖片預(yù)留空間,結(jié)合Core Graphic 來(lái)繪圖。
單排
思路
根據(jù) Config 圖片的寬高,設(shè)置 CTRunDelegateCallbacks
生成 runDelegate
找到要插入圖片的位置,將圖片信息封裝成一個(gè) attributedString 富文本類型的占位符
富文本類型的占位符
struct BKCoreTextData {
let ctFrame : CTFrame
let size : CGSize
let imageUrl: String
}
struct BKCoreTextConfig {
let size : CGSize
}
static func parse(content: String, imageUrl: String, config: BKCoreTextConfig) -> BKCoreTextData {
var imageCallback = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (refCon) -> Void in
}, getAscent: { ( refCon) -> CGFloat in
return config.size.height // 高度
}, getDescent: { (refCon) -> CGFloat in
return 0 // 底部距離
}) { (refCon) -> CGFloat in
return config.size.width // 寬度
}
var imageName = "avatar"
let runDelegate = CTRunDelegateCreate(&imageCallback,&imageName)
// 富文本類型的占位符
let imageAttributedString = NSMutableAttributedString(string: " ")
imageAttributedString.addAttribute(NSAttributedStringKey(rawValue:
kCTRunDelegateAttributeName as String), value: runDelegate!, range:
NSMakeRange(0, 1))
imageAttributedString.addAttribute(NSAttributedStringKey(rawValue:
"avatarImage"), value: imageName, range: NSMakeRange(0, 1))
// 富文本類型的占位符插到要顯示圖片的位置
// 這里的設(shè)定是圖片插在文本行首。。。
content.insert(imageAttributedString, at: 0)
// 文本繪制同上 多了一個(gè)imageUrl信息
let data: BKCoreTextData = ...
return data
}
圖片和文本混合怎么顯示???
思路
遍歷 CTLine
遍歷每個(gè) Line 中 CTRun
通過(guò) CTRunGetAttributes 得到所有屬性
通過(guò) KVC 取得屬性中的代理屬性,圖片占位符綁定了代理
判斷是否之前設(shè)置的圖片代理來(lái)區(qū)分文本和圖片
獲取圖片 距離原點(diǎn)偏移量 來(lái)計(jì)算圖片繪制區(qū)域的 CGRect
使用 Core Graphics 異步繪制圖片
var data: BKCoreTextData? {
didSet { setNeedsDisplay() }
}
private var avatarImage: Image = #默認(rèn)占位符
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
guard let frame = data?.ctFrame else { return }
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: bounds.size.height)
context.scaleBy(x: 1, y: -1)
CTFrameDraw(frame, context)
let lines = CTFrameGetLines(frame) as Array
let count = lines.count
var origins = [CGPoint].init(repeating: CGPoint(0, 0), count: count)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
for (index, line) in lines.enumerated() {
(CTLineGetGlyphRuns(line as! CTLine) as Array).forEach({
var runAscent : CGFloat = 0
var runDescent : CGFloat = 0
let lineOrigin = origins[index]
let attributes = CTRunGetAttributes($0 as! CTRun)
let width = CGFloat( CTRunGetTypographicBounds($0 as!
CTRun, CFRangeMake(0,0), &runAscent, &runDescent, nil))
let location = CTLineGetOffsetForStringIndex(line as! CTLine,
CTRunGetStringRange($0 as! CTRun).location, nil)
let runRect = ccr(lineOrigin.x + location, lineOrigin.y - runDescent,
width, runAscent + runDescent)
let imageNames = attributes.object(forKey: "avatarImage")
if imageNames is String {
DispatchQueue.global().async { [weak self] in
// 獲取圖片 data.imageUrl
let tmp = ....
DispatchQueue.main.async {
self?.avatarImage = tmp!
self?.setNeedsDisplay(runRect)
}
}
context.draw(avatarImage.cgImage!, in: runRect)
}
})
}
}
這是比較理想的圖文混合,圖片的高度和文本高度差不多,以及圖片的位置又剛好在行首。
組隊(duì)
來(lái)看個(gè)實(shí)際需求的圖文混合
繪制的方式是和上面的是一樣的,不同在于圖片和文本的排版不一樣。
實(shí)踐步驟:
0.不做處理
1.文字都堆在一起了,給文本不同樣式劃分段落
2.恩,跟目標(biāo)很接近了 ??,根據(jù)不同段落展示樣式,調(diào)整行首縮進(jìn)距離 firstLineHeadIndent 以及基線的距離 baselineOffset
let margin : CGFloat = 20
let paragraphStyle0 = NSMutableParagraphStyle()
paragraphStyle0.alignment = .left
paragraphStyle0.firstLineHeadIndent = image.size.width + margin // 首行縮進(jìn)
title.addAttributes([.baselineOffset: 15,.paragraphStyle: paragraphStyle0], range: NSMakeRange(0, title.length - 1))
subTitle.addAttributes([.baselineOffset: 10, .paragraphStyle: paragraphStyle0], range: NSMakeRange(0, subTitle.length))
let paragraphStyle1 = NSMutableParagraphStyle()
paragraphStyle1.alignment = .left
paragraphStyle1.firstLineHeadIndent = kScreenWidth - 30 - 20
indicator.addAttributes([.baselineOffset: 28, .paragraphStyle: paragraphStyle1], range: NSMakeRange(0, 1))
3.貌似已經(jīng)達(dá)到目的了,這里也可以體現(xiàn)使用 Core Text 的優(yōu)勢(shì),減少不必要的圖層。
但是呢,這只是當(dāng)前場(chǎng)景一種取巧的方式,hard code 間距,基線距離。如果 title 或者 subTitle 多行,這種方法就失效了。所以類似問(wèn)題就是要解決圖片和文字環(huán)繞的排版方式。
圖文環(huán)繞
draw 函數(shù)是直接調(diào)用 frame 將內(nèi)容繪制出來(lái)的,frame 是怎么來(lái)的
let path = CGMutablePath()
path.addRect(CGRect(x: 0, y: 0, width: config.width, height: height))
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
frame 是根據(jù)指定的 path 生成的,所以如果這個(gè) path 將圖片區(qū)域去掉,得到的 frame 就不包含該區(qū)域。但是這個(gè) frame 里面也不再包含圖片信息了。
let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height))
// !!! 這個(gè)左下角為坐標(biāo)原點(diǎn)
let imagePath = UIBezierPath(rect: CGRect(x: 3, y: 3, width: image.size.width, height: image.size.height))
// 減去圖片區(qū)域
path.append(imagePath)
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path.cgPath, nil)
可以看到整個(gè)繪制區(qū)域被分成了兩個(gè)部分,一個(gè)是圖片一個(gè)是文本。通過(guò) UIBezierPath 還可以繪制任何想要的形狀。剩下的問(wèn)題就是處理段落之間的行間距。
Done!
總結(jié)
本文只是簡(jiǎn)單介紹了一些 Core Text 的東西,實(shí)際上還是有許多的細(xì)節(jié)還需要細(xì)細(xì)磨。實(shí)際開(kāi)發(fā)過(guò)程中可能業(yè)務(wù)的形式不一,但是知識(shí)點(diǎn)是相通的,靈活應(yīng)用都能達(dá)到目的。希望本文能給使用 CoreText 的同學(xué)一些啟發(fā)。
參考
http://blog.devtang.com/2015/06/27/using-coretext-1/
http://blog.devtang.com/2015/06/27/using-coretext-2/
https://www.raywenderlich.com/153591/core-text-tutorial-ios-making-magazine-app
http://blog.cnbang.net/tech/2729/