CoreText是一個進階的比較底層的布局文本和處理字體的技術,CoreText API在OS X v10.5 和 iOS3.2時引入,在OS X 和iOS 環境下均可以使用。
不是特別復雜的需求一般情況下UILabel、UITextView都可以搞定,他們是Apple幫我們封裝好的顯示文本的控件,但是像復雜的圖文,鏈接識別并替換成 "點擊鏈接" , @someone 綁定 ,電話識別,文字大小不一 ,當然這些可以使用UIWebView,但是CoreText 技術相對于 UIWebView,有著更少的內存占用,以及可以在后臺渲染的優點,非常適合用于內容的排版工作。CoreText 提供了非常高的靈活性,但是操作起來的比較復雜,學技術不就應該找最難的攻克嗎?
來看一張框架圖
要學習CoreText 首先得了解屬性字---NSAttributedString
或者 NSMutableAttributedString
NSAttributedString 和 NSMutableAttributedString
NSAttributedString是一個帶有屬性的字符串,通過該類可以靈活地操作和呈現多種樣式的文字數據
可以事先定義好屬性 然后加到文字上
let str = "這是一段用來測試的字符串 this is a string for test"
let dic = [NSFontAttributeName:UIFont.boldSystemFontOfSize(20),
NSForegroundColorAttributeName:UIColor.redColor()]
let attrStr = NSAttributedString(string: str, attributes: dic)
label.attributedText = attrStr
效果
可以看到我們并沒有給label設置顏色和字體,創建了一個帶兩個屬性的NSAttributedString
, 沒有使用label.text
而是 label.attributedText
如果只能給所有的文字設置一樣的屬性,那這個屬性字也太沒勁了。我們可以給一段文字設置不同的屬性
let mutableAttrStr = NSMutableAttributedString(string: str)
mutableAttrStr.addAttributes(dic, range: NSMakeRange(0, 2))
mutableAttrStr.addAttributes([NSFontAttributeName:UIFont.systemFontOfSize(13),NSUnderlineStyleAttributeName: 1 ], range: NSMakeRange(2,8))
label.attributedText = mutableAttrStr
我們可以給不同位置的文字指定不同的樣式,位置通過NSRange給出(NSRange是一個結構體,有兩個參數 一個location 一個 length ,這兩個參數可以唯一的確定一段字串)
效果
這樣我們就給前面的文字設置了兩種不同的屬性。
那么我們可以設置哪些屬性呢。
- NSFontAttributeName 設置字體屬性,默認值:字體:Helvetica(Neue) 字號12
- NSForegroundColorAttributeName 設置字體顏色,取值為 UIColor對象,默認值為
- NSBackgroundColorAttributeName 設置字體所在區域背景顏色,取值為 UIColor對象,默認值為nil, 透明
- NSLigatureAttributeName 設置連體屬性,取值為NSNumber 對象(整數),0 表示沒有連體字符,1 表示使用默認的連體字符
- NSKernAttributeName 設定字符間距,取值為 NSNumber 對象(整數),正值間距加寬,負值間距變窄
- NSStrikethroughStyleAttributeName 設置刪除線,取值為 NSNumber 對象(整數)
- NSStrikethroughColorAttributeName 設置刪除線顏色,取值為 UIColor 對象,默認值為黑色
- NSUnderlineStyleAttributeName 設置下劃線,取值為 NSNumber 對象(整數),枚舉常量 NSUnderlineStyle中的值,與刪除線類似NSUnderlineColorAttributeName 設置下劃線顏色,取值為 UIColor 對象,默認值為黑色
- NSStrokeWidthAttributeName 設置筆畫寬度,取值為 NSNumber 對象(整數),負值填充效果,正值中空效果
- NSStrokeColorAttributeName 填充部分顏色,不是字體顏色,取值為 UIColor 對象NSShadowAttributeName 設置陰影屬性,取值為 NSShadow 對象
- NSTextEffectAttributeName 設置文本特殊效果,取值為 NSString 對象,目前只有圖版印刷效果可用:
- NSBaselineOffsetAttributeName 設置基線偏移值,取值為 NSNumber (float),正值上偏,負值下偏
- NSObliquenessAttributeName 設置字形傾斜度,取值為 NSNumber (float),正值右傾,負值左傾
- NSExpansionAttributeName 設置文本橫向拉伸屬性,取值為 NSNumber (float),正值橫向拉伸文本,負值橫向壓縮文本
- NSWritingDirectionAttributeName 設置文字書寫方向,從左向右書寫或者從右向左書寫
- NSVerticalGlyphFormAttributeName 設置文字排版方向,取值為 NSNumber 對象(整數),0 表示橫排文本,1 表示豎排文本
- NSLinkAttributeName 設置鏈接屬性,點擊后調用瀏覽器打開指定URL地址
- NSAttachmentAttributeName 設置文本附件,取值為NSTextAttachment對象,常用于文字圖片混排
- NSParagraphStyleAttributeName 設置文本段落排版格式,取值為 NSParagraphStyle 對象
這么多屬性使用的也記不住,使用的時候自行google用法,還是很強大的??!!
屬性字就介紹這么多,簡單富文本使用它就能搞定。下面介紹我們的主角--CoreText
CoreText
屬性字是用來給文本設置樣式,那么CoreText就是用來給文本進行排版的,可以自定每行高度,每個字符占位 等等
CoreText 是用于處理文字和字體的底層技術。它直接和 Core Graphics(又被稱為 Quartz)打交道。Quartz 是一個 2D 圖形渲染引擎。Quartz 能夠直接處理字體(font)和字形(glyphs),將文字渲染到界面上,它是基礎庫中唯一能夠處理字形的模塊。因此,CoreText 為了排版,需要將顯示的文本內容、位置、字體、字形直接傳遞給 Quartz。相比其它 UI 組件,由于 CoreText 直接和 Quartz 來交互,所以它具有高速的排版效果。
我們來看一個CoreText對象模型圖
來一段枯燥的講解(后面會有??的)
如上圖所述,其中Framesetter對應的類型是CTFramesetter
,通過CFAttributedString(NSAttributeString 也可以無縫橋接)進行初始化,它作為CTFrame對象的生產工廠,負責根據path生產對應的CTFrame。CTFrame是可以通過CTFrameDraw函數直接繪制到context上的,當然你可以在繪制之前,操作CTFrame中的CTLine,進行一些參數的微調。CTLine 可以看做Core Text繪制中的一行的對象 通過它可以獲得當前行的line ascent,line descent ,line leading,還可以獲得Line下的所有Glyph Runs。CTRun 或者叫做 Glyph Run,是一組共享相同attributes(屬性)的字形的集合體。CTFrame是指整個該UIView子控件的繪制區域,CTLine則是指每一行,CTRun則是每一段具有一樣屬性的字符串。比如某段字體大小、顏色都一致的字符串為一個CTRun,CTRun不可以跨行,不管屬性一致或不一致。通常的結構是每一個CTFrame有多個CTLine,每一個CTLine有多個CTRun。
由于CoreText
一開始便是定位于桌面的排版系統,所以使用了傳統的原點在左下角的坐標系,所以它在繪制文本的時候都是參照左下角的原點進行繪制的。
如果你啥也不做處理,直接在這個context上進行CoreText繪制,你會發現文字是鏡像且上下顛倒。
??來啦!先來看一個最簡單的CoreText使用的例子
一個簡單的例子
首先新建一個CTView繼承自UIView
import UIKit
class CTView: UIView {
override func drawRect(rect: CGRect) {
super.drawRect(rect)
// 1
let context = UIGraphicsGetCurrentContext()
// 2
CGContextSetTextMatrix(context, CGAffineTransformIdentity)
CGContextTranslateCTM(context, 0, self.bounds.size.height)
CGContextScaleCTM(context, 1.0, -1.0)
// 3
let path = CGPathCreateMutable()
CGPathAddRect(path, nil, self.bounds)
// 4
let attrString = NSAttributedString(string:"Hello CoreText!")
let framesetter = CTFramesetterCreateWithAttributedString(attrString)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, nil)
// 5
CTFrameDraw(frame,context!)
}
}
然后把這個view加在ViewController上
let ctView = CTView()
ctView.frame = CGRectMake(10, 150, self.view.bounds.width - 20, 200)
ctView.backgroundColor = UIColor.whiteColor()
self.view.addSubview(ctView)
運行效果:
解釋:
1、 通過UIGraphisGetCurrentContext()
獲取當前的環境
2、將坐標系上下翻轉。對于底層的繪制引擎來說,屏幕的左下角是(0, 0)坐標。而對于上層的 UIKit 來說,左上角是 (0, 0) 坐標。所以我們為了之后的坐標系描述按 UIKit 來做,所以先在這里做一個坐標系的上下翻轉操作。翻轉之后,底層和上層的 (0, 0) 坐標就是重合的了。
3、創建繪制區域CGPathCreateMutable(),CoreText 本身支持各種文字排版的區域,我們這里簡單地將 UIView 的整個界面作為排版的區域。
當然這里如果覺得CGMutablePath 不好用 可以選擇使用更方便的UIBezierPath來操作排版區域.
把上文中的3改成
let path1 = UIBezierPath(roundedRect: self.bounds, cornerRadius:self.bounds.size.width/2 )
把4改成 順便給文字加了點屬性
// 4
let attrString = "Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!"
let mutableAttrStr = NSMutableAttributedString(string: attrString)
mutableAttrStr.addAttributes([NSFontAttributeName:UIFont.systemFontOfSize(20),
NSForegroundColorAttributeName:UIColor.redColor() ], range: NSMakeRange(0, 20))
mutableAttrStr.addAttributes([NSFontAttributeName:UIFont.systemFontOfSize(13),NSUnderlineStyleAttributeName: 1 ], range: NSMakeRange(20,18))
let framesetter = CTFramesetterCreateWithAttributedString(mutableAttrStr)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, mutableAttrStr.length), path1.CGPath, nil)
排版區域就變了 而且使用了UIBezierPath的API
4、根據AttributedString生成CTFramesetterRef,根據framesetter和繪圖區域創建CTFrame
5、使用CTFrameDraw進行繪制(后面復雜的可能不會直接畫frame 而是選擇一行一行的畫 )
圖文混排
CoreText如果只能定義文本繪制區域,那就太沒勁了,CoreText還可以支持圖文混排,本地圖片和網絡圖片。
CoreText從繪制純文本到繪制圖片,依然是使用NSAttributedString,只不過圖片的實現方式是用一個空白字符作為在NSAttributedString中的占位符,然后設置代理,告訴CoreText給該占位字符留出一定的寬高。最后把圖片繪制到預留的位置上。
圖中 第一個圖片是存在本地,第二個圖片是來自網絡。下面看看怎么做。代碼有點長 但是思路應該清晰。
import UIKit
class CTPicTxtView: UIView {
var image:UIImage?
override func drawRect(rect: CGRect) {
super.drawRect(rect)
// 1 獲取上下文
let context = UIGraphicsGetCurrentContext()
// 2 轉換坐標
CGContextSetTextMatrix(context, CGAffineTransformIdentity)
CGContextTranslateCTM(context, 0, self.bounds.size.height)
CGContextScaleCTM(context, 1.0, -1.0)
// 3 繪制區域
let path = UIBezierPath(rect: rect)
// 4 創建需要繪制的文字
let attrString = "Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!Hello CoreText!"
let mutableAttrStr = NSMutableAttributedString(string: attrString)
mutableAttrStr.addAttributes([NSFontAttributeName:UIFont.systemFontOfSize(20),
NSForegroundColorAttributeName:UIColor.redColor() ], range: NSMakeRange(0, 5))
mutableAttrStr.addAttributes([NSFontAttributeName:UIFont.systemFontOfSize(13),NSUnderlineStyleAttributeName: 1 ], range: NSMakeRange(3,10))
let style = NSMutableParagraphStyle() //用來設置段落樣式
style.lineSpacing = 6 //行間距
mutableAttrStr.addAttributes([NSParagraphStyleAttributeName:style], range: NSMakeRange(0, mutableAttrStr.length))
// 5 為圖片設置CTRunDelegate,delegate決定留給圖片的空間大小
var imageName = "mc"
var imageCallback = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (refCon) -> Void in
}, getAscent: { ( refCon) -> CGFloat in
// let imageName = "mc"
// refCon.initialize()
// let image = UIImage(named: imageName)
return 100 //返回高度
}, getDescent: { (refCon) -> CGFloat in
return 50 //返回底部距離
}) { (refCon) -> CGFloat in
// let imageName = String("mc")
// let image = UIImage(named: imageName)
return 100 //返回寬度
}
let runDelegate = CTRunDelegateCreate(&imageCallback, &imageName)
let imgString = NSMutableAttributedString(string: " ") // 空格用于給圖片留位置
imgString.addAttribute(kCTRunDelegateAttributeName as String, value: runDelegate!, range: NSMakeRange(0, 1)) //rundelegate 占一個位置
imgString.addAttribute("imageName", value: imageName, range: NSMakeRange(0, 1))//添加屬性,在CTRun中可以識別出這個字符是圖片
mutableAttrStr.insertAttributedString(imgString, atIndex: 15)
//網絡圖片相關
var imageCallback1 = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (refCon) -> Void in
}, getAscent: { ( refCon) -> CGFloat in
return 70 //返回高度
}, getDescent: { (refCon) -> CGFloat in
return 50 //返回底部距離
}) { (refCon) -> CGFloat in
return 100 //返回寬度
}
var imageUrl = "http://img3.3lian.com/2013/c2/64/d/65.jpg" //網絡圖片鏈接
let urlRunDelegate = CTRunDelegateCreate(&imageCallback1, &imageUrl)
let imgUrlString = NSMutableAttributedString(string: " ") // 空格用于給圖片留位置
imgUrlString.addAttribute(kCTRunDelegateAttributeName as String, value: urlRunDelegate!, range: NSMakeRange(0, 1)) //rundelegate 占一個位置
imgUrlString.addAttribute("urlImageName", value: imageUrl, range: NSMakeRange(0, 1))//添加屬性,在CTRun中可以識別出這個字符是圖片
mutableAttrStr.insertAttributedString(imgUrlString, atIndex: 50)
// 6 生成framesetter
let framesetter = CTFramesetterCreateWithAttributedString(mutableAttrStr)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, mutableAttrStr.length), path.CGPath, nil)
// 7 繪制除圖片以外的部分
CTFrameDraw(frame,context!)
// 8 處理繪制圖片邏輯
let lines = CTFrameGetLines(frame) as NSArray //存取frame中的ctlines
let ctLinesArray = lines as Array
var originsArray = [CGPoint](count:ctLinesArray.count, repeatedValue: CGPointZero)
let range: CFRange = CFRangeMake(0, 0)
CTFrameGetLineOrigins(frame,range,&originsArray)
//遍歷CTRun找出圖片所在的CTRun并進行繪制,每一行可能有多個
for i in 0..<lines.count{
//遍歷每一行CTLine
let line = lines[i]
var lineAscent = CGFloat()
var lineDescent = CGFloat()
var lineLeading = CGFloat()
//該函數除了會設置好ascent,descent,leading之外,還會返回這行的寬度
CTLineGetTypographicBounds(line as! CTLineRef, &lineAscent, &lineDescent, &lineLeading)
let runs = CTLineGetGlyphRuns(line as! CTLine) as NSArray
for j in 0..<runs.count{
// 遍歷每一個CTRun
var runAscent = CGFloat()
var runDescent = CGFloat()
let lineOrigin = originsArray[i]// 獲取該行的初始坐標
let run = runs[j] // 獲取當前的CTRun
let attributes = CTRunGetAttributes(run as! CTRun) as NSDictionary
let width = CGFloat( CTRunGetTypographicBounds(run as! CTRun, CFRangeMake(0,0), &runAscent, &runDescent, nil))
let runRect = CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line as! CTLine, CTRunGetStringRange(run as! CTRun).location, nil), lineOrigin.y - runDescent, width, runAscent + runDescent)
let imageNames = attributes.objectForKey("imageName")
let urlImageName = attributes.objectForKey("urlImageName")
if imageNames is NSString {
//本地圖片
let image = UIImage(named: imageName as String)
let imageDrawRect = CGRectMake(runRect.origin.x, lineOrigin.y-runDescent, 100, 100)
CGContextDrawImage(context, imageDrawRect, image?.CGImage)
}
if let urlImageName = urlImageName as? String{
var image:UIImage?
let imageDrawRect = CGRectMake(runRect.origin.x, lineOrigin.y-runDescent, 100, 100)
if self.image == nil{
image = UIImage(named:"hs") //灰色圖片占位
//去下載
if let url = NSURL(string: urlImageName){
let request = NSURLRequest(URL: url)
NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { (data, resp, err) -> Void in
if let data = data{
dispatch_sync(dispatch_get_main_queue(), { () -> Void in
self.image = UIImage(data: data)
self.setNeedsDisplay() //下載完成會重繪
})
}
}).resume()
}
}else{
image = self.image
}
CGContextDrawImage(context, imageDrawRect, image?.CGImage)
}
}
}
}
}
1、2、3、4和前面簡單Demo是一模一樣的 ,只是加了個行間距。
5、 這塊用一個回調設置了圖片大小等信息 ,然會創建了一個CTRun的代理,創建一個空白占位字符,給它加了個屬性,后面繪制的時候好識別。 最后把這個占位符加到我們屬性文本的某個位置。
地下網絡圖片 name換成了url 其他如法炮制
6、7和上小結一樣的
8、 正式開始處理圖片部分
根據CTFrame 獲取 CTLine 獲取 originsArray 每一行的原點,用來定位 。根據CTLine獲取到 CTRun 。 CTRun是每一個相同屬性字符串 ,但是不會隔行。
遍歷CTRun 根據我們前面設置的屬性 找到本地圖片進行繪制
網絡圖片的繪制也很簡單如果沒有下載 先放個灰色的圖占位,然后去下載,下載好了 賦值給self的一個變量 ,然后重繪就OK了 關于NSURLSession不會使用的可以看我的另一篇文章。NSURLSession
關于CoreText還有很多,逐行排版 文字和emoji 混排問題 , 連接識別 ,點擊圖片 點擊連接等等。。篇幅有點長。。下一篇接著介紹,這篇先到這邊。
本文實例代碼已上傳github: https://github.com/smalldu/ZZCoreTextDemo