本篇是續上篇 Swift之CoreText排版神器(長篇高能) ,沒看上篇基礎篇的建議從上篇看起.
上篇我們介紹了NSAttributeString屬性字和CoreText的簡單使用,以及簡單的圖文混排。這節我們使用CoreText來解決些其他問題。
先來看一副圖
如圖,Origin那塊是基線相當于原點,descent是向下的 一般是負值,ascent是正值,還有lineHeight和capHeight還有x-height都在圖中標出,那在代碼中如何獲取這些值呢?
let font = UIFont.systemFontOfSize(14)
print(font.descender) //-3.376953125
print(font.ascender) //13.330078125
print(font.lineHeight) //16.70703125
print(font.capHeight) //9.8642578125
print(font.xHeight) //7.369140625
print(font.leading) //0.0
這里定義一個14號的文字 取出對應的各個值。這些都是只讀的
純文本排版的時候會有一些細節問題,我們先來繪制一個帶有中文,英文,數字以及emoji表情的文本看看效果。
可以看出在含有emoji的那幾行占有的高度會比較高,空隙比較大。所以如果按boundingRectWithSize
根據字體大小和寬度來計算文本高度的方法就不行了,而且這樣排版看起來也不是很美觀,這樣我們就不能直接用CTFrameDraw
來繪制了,可能需要給定行高,一行一行繪制 ,使用CTLineDraw
。
首先我們需要計算出文字所占的Size.
let SCREEN_WIDTH:CGFloat = UIScreen.mainScreen().bounds.size.width //屏幕寬度
/**
計算Size
- parameter txt: 文本
- returns: size
*/
func sizeForText(mutableAttrStr:NSMutableAttributedString)->CGSize{
//創建CTFramesetterRef實例
let frameSetter = CTFramesetterCreateWithAttributedString(mutableAttrStr)
// 獲得要繪制區域的高度
let restrictSize = CGSizeMake(SCREEN_WIDTH-20, CGFloat.max)
let coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0) , nil, restrictSize, nil)
return coreTextSize
}
很簡單 ,根據屬性字得到framesetter 然后再根據framesetter計算出所占的size。
得到size后要怎么操作呢?
這塊我貼下所有代碼 注釋很詳細
import UIKit
class CTextView: UIView {
let SCREEN_WIDTH:CGFloat = UIScreen.mainScreen().bounds.size.width //屏幕寬度
let SCREEN_HEIGHT:CGFloat = UIScreen.mainScreen().bounds.size.height //屏幕高度
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 = "來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-蘭emoji??????????????????????水電費洛杉磯大立科技??????????????索拉卡叫我??????????sljwolw19287812來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這"
// 5 設置frame
let mutableAttrStr = NSMutableAttributedString(string: attrString)
let framesetter = CTFramesetterCreateWithAttributedString(mutableAttrStr)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, mutableAttrStr.length), path.CGPath, nil)
// 6 取出CTLine 準備一行一行繪制
let lines = CTFrameGetLines(frame)
let lineCount = CFArrayGetCount(lines)
var lineOrigins:[CGPoint] = Array(count:lineCount,repeatedValue:CGPointZero)
//把frame里每一行的初始坐標寫到數組里,注意CoreText的坐標是左下角為原點
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0),&lineOrigins)
//獲取屬性字所占的size
let size = sizeForText(mutableAttrStr)
let height = size.height
let font = UIFont.systemFontOfSize(14)
var frameY:CGFloat = 0
// 計算每行的高度 (總高度除以行數)
let lineHeight = height/CGFloat(lineCount)
for i in 0..<lineCount{
let lineRef = unsafeBitCast(CFArrayGetValueAtIndex(lines,i), CTLineRef.self)
var lineAscent:CGFloat = 0
var lineDescent:CGFloat = 0
var leading:CGFloat = 0
//該函數除了會設置好ascent,descent,leading之外,還會返回這行的寬度
CTLineGetTypographicBounds(lineRef, &lineAscent, &lineDescent, &leading)
var lineOrigin = lineOrigins[i]
//計算y值(注意左下角是原點)
frameY = height - CGFloat(i + 1)*lineHeight - font.descender
//設置Y值
lineOrigin.y = frameY
//繪制
CGContextSetTextPosition(context,lineOrigin.x, lineOrigin.y)
CTLineDraw(lineRef, context!)
//調整坐標
frameY = frameY - lineDescent
}
//
// CTFrameDraw(frame,context!)
}
/**
計算Size
- parameter txt: 文本
- returns: size
*/
func sizeForText(mutableAttrStr:NSMutableAttributedString)->CGSize{
//創建CTFramesetterRef實例
let frameSetter = CTFramesetterCreateWithAttributedString(mutableAttrStr)
// 獲得要繪制區域的高度
let restrictSize = CGSizeMake(SCREEN_WIDTH-20, CGFloat.max)
let coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0) , nil, restrictSize, nil)
return coreTextSize
}
}
前面5步和前一個小結是一樣的,這次拿到CTFrame后我們并沒有直接調用CTFrameDraw
來繪制。而是獲取所有的CTLine,拿到CTLine后再計算總高度。根據總高度計算每行高度,然后循環CTLine,獲取每個Line計算Y指標的值,逐行設置位置然后Draw上去。
CGContextSetTextPosition(context,lineOrigin.x, lineOrigin.y)
CTLineDraw(lineRef, context!)
看下對比效果
左邊是直接畫的,右邊是逐行計算后畫的,等高了,看起來不會有層次不齊的感覺了!
下面看看怎么自動識別連接等
實現的思路主要是給控件添加手勢點擊并進行監聽,在用戶點擊時拿到點擊的位置,并在手勢識別結束后用CoreText遍歷每一個CTLine,判斷點擊的位置是否在識別的特定字符串內,如果是則找出該字符串。使CTLineGetStringIndexForPosition函數來找出點擊的字符位于整個字符串的位置。
首先寫兩個正則來檢測文本中的@和鏈接, 如果您有任何需要檢測的都可以添加正則去實現。
//url的正則
let regex_url = "(http|ftp|https):\\/\\/[\\w\\-_]+(\\.[\\w\\-_]+)+([\\w\\-\\.,@?^=%&:/~\\+#]*[\\w\\-\\@?^=%&/~\\+#])?"
let regex_someone = "@[^\\s@]+?\\s{1}"
關于正則表達式,不再本文討論范圍內,自行google....
然后就要根據正則匹配字符串。返回對應的range并且修改屬性字的顏色。
//識別特定字符串并改其顏色,返回識別到的字符串所在的range
func recognizeSpecialStringWithAttributed(attrStr:NSMutableAttributedString)->[NSRange]{
// 1
var rangeArray = [NSRange]()
//識別人名字
// 2
let atRegular = try? NSRegularExpression(pattern: regex_someone, options: NSRegularExpressionOptions.CaseInsensitive) //不區分大小寫的
// 3
let atResults = atRegular?.matchesInString(attrStr.string, options: NSMatchingOptions.WithTransparentBounds , range: NSMakeRange(0, attrStr.length))
// 4
for checkResult in atResults!{
attrStr.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(checkResult.range.location, checkResult.range.length))
rangeArray.append(checkResult.range)
}
//識別鏈接
let atRegular1 = try? NSRegularExpression(pattern: regex_url, options: NSRegularExpressionOptions.CaseInsensitive) //不區分大小寫的
let atResults1 = atRegular1?.matchesInString(attrStr.string, options: NSMatchingOptions.WithTransparentBounds , range: NSMakeRange(0, attrStr.length))
for checkResult in atResults1!{
attrStr.addAttribute(NSForegroundColorAttributeName, value: UIColor.blueColor(), range: NSMakeRange(checkResult.range.location, checkResult.range.length))
rangeArray.append(checkResult.range)
}
return rangeArray
}
我這里就識別了@和鏈接,大家自行添加,這里為了方便并沒有封裝,大家可以封裝下,使用一個結構體,有NSRange 數組 和type類型 或者字典類型,自由發揮。這里只做識別。稍微解釋下代碼
1、定義一個數組存放匹配的Range集合
2、根據前面定義的正則創建一個正則表達式對象,這里會拋出異常為了方便,并沒有處理。option這里使用了CaseInsensitive不區分大小寫。還有很多別的選項。直接command+點擊看。
3、拿到匹配的結果集,包含range屬性(我們需要的)
4、循環添加到數組,并給這個range的字符修改顏色屬性。
解析方法準備好之后就可以開始繪制了,和前面的大同小異
let SCREEN_WIDTH:CGFloat = UIScreen.mainScreen().bounds.size.width //屏幕寬度
let SCREEN_HEIGHT:CGFloat = UIScreen.mainScreen().bounds.size.height //屏幕高度
var lineHeight:CGFloat = 0
var ctFrame:CTFrameRef?
var spcialRanges = [NSRange]()
//url的正則
let regex_url = "(http|ftp|https):\\/\\/[\\w\\-_]+(\\.[\\w\\-_]+)+([\\w\\-\\.,@?^=%&:/~\\+#]*[\\w\\-\\@?^=%&/~\\+#])?"
let regex_someone = "@[^\\s@]+?\\s{1}"
let str = "來一段數 @sd圣誕節 字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl http://www.baidu.com 是電話費卡刷卡來這來一段數字,文本emoji http://www.zuber.im 的哈哈哈29993002-309-sdflslsfl是電話費卡 @kakakkak 刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-蘭emoji??????????????????????水電費洛杉磯大立科技??????????????索拉卡叫我??????????sljwolw19287812來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這來一段數字,文本emoji的哈哈哈29993002-309-sdflslsfl是電話費卡刷卡來這"
var pressRange:NSRange?
var mutableAttrStr:NSMutableArray!
var selfHeight:CGFloat = 0
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 創建需要繪制的文字
// 5 設置frame
let mutableAttrStr = NSMutableAttributedString(string: str)
// 獲取特殊字符range 繪制特殊字符
self.spcialRanges = recognizeSpecialStringWithAttributed(mutableAttrStr)
let framesetter = CTFramesetterCreateWithAttributedString(mutableAttrStr)
ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, mutableAttrStr.length), path.CGPath, nil)
// 6 取出CTLine 準備一行一行繪制
let lines = CTFrameGetLines(ctFrame!)
let lineCount = CFArrayGetCount(lines)
var lineOrigins:[CGPoint] = Array(count:lineCount,repeatedValue:CGPointZero)
//把frame里每一行的初始坐標寫到數組里,注意CoreText的坐標是左下角為原點
CTFrameGetLineOrigins(ctFrame!, CFRangeMake(0, 0),&lineOrigins)
//獲取屬性字所占的size
let size = sizeForText(mutableAttrStr)
let height = size.height
// self.frame.size.height = height
let font = UIFont.systemFontOfSize(14)
var frameY:CGFloat = 0
// 計算每行的高度 (總高度除以行數)
lineHeight = height/CGFloat(lineCount)
for i in 0..<lineCount{
let lineRef = unsafeBitCast(CFArrayGetValueAtIndex(lines,i), CTLineRef.self)
var lineAscent:CGFloat = 0
var lineDescent:CGFloat = 0
var leading:CGFloat = 0
//該函數除了會設置好ascent,descent,leading之外,還會返回這行的寬度
CTLineGetTypographicBounds(lineRef, &lineAscent, &lineDescent, &leading)
var lineOrigin = lineOrigins[i]
//計算y值(注意左下角是原點)
frameY = height - CGFloat(i + 1)*lineHeight - font.descender
//設置Y值
lineOrigin.y = frameY
//繪制
CGContextSetTextPosition(context,lineOrigin.x, lineOrigin.y)
CTLineDraw(lineRef, context!)
//調整坐標
frameY = frameY - lineDescent
}
}
這段代碼和之前差不多,只不過把一些變量放在外面定義以便別的方法使用。還有就是加上了那個解析的方法
self.spcialRanges = recognizeSpecialStringWithAttributed(mutableAttrStr)
在ViewController中調用
let ctURLView = CTURLView()
ctURLView.frame = CGRectMake(10, 100, self.view.bounds.width - 20, 300)
ctURLView.backgroundColor = UIColor.grayColor()
let mutableAttrStr = NSMutableAttributedString(string: ctURLView.str)
let size = ctURLView.sizeForText(mutableAttrStr)
ctURLView.frame.size = size
self.view.addSubview(ctURLView)
這邊要先計算下size
看下效果
不錯吧 并沒有多少代碼就識別出來了,下面看看處理點擊事件
首先注冊一個tap手勢并設置下代理
override init(frame: CGRect) {
super.init(frame: frame)
//添加手勢
let tap = UITapGestureRecognizer(target: self, action: "tap:")
tap.delegate = self
self.addGestureRecognizer(tap)
}
然后實現手勢方法和代理
extension CTURLView:UIGestureRecognizerDelegate{
func tap(gesture:UITapGestureRecognizer){
if gesture.state == .Ended{
let nStr = self.str as NSString
let pressStr = nStr.substringWithRange(self.pressRange!)
print(pressStr)
}
}
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
//點擊處在特定字符串內才進行識別
var gestureShouldBegin = false
// 1
let location = gestureRecognizer.locationInView(self)
// 2
let lineIndex = Int(location.y/lineHeight)
print("你點擊了第\(lineIndex)行")
// 3 把點擊的坐標轉換為CoreText坐標系下
let clickPoint = CGPointMake(location.x, lineHeight-location.y)
let lines = CTFrameGetLines(self.ctFrame!);
let lineCount = CFArrayGetCount(lines)
if lineIndex < lineCount{
let clickLine = unsafeBitCast(CFArrayGetValueAtIndex(lines,lineIndex), CTLineRef.self)
// 4 點擊的index
let startIndex = CTLineGetStringIndexForPosition(clickLine, clickPoint)
print("strIndex = \(startIndex)")
// 5
for range in self.spcialRanges{
if startIndex >= range.location && startIndex <= range.location + range.length{
gestureShouldBegin = true
self.pressRange = range
print(range)
}
}
}
return gestureShouldBegin
}
}
gestureRecognizerShouldBegin是一個代理方法,在這個方法中來檢測特殊字符串
1、拿到觸摸點在當前view的位置
2、根據y值和行高的比獲取line編號,lineHeight是一個全局變量。可以下載本文實例代碼對照看
3、轉成CoreText下坐標
4、獲取字符的index
5、循環特殊字符的range查看index是否在range中 如果在range中 把當前按下的range賦值為此range,打印出來。
這時候 在tap方法中取出這個range對應的字符串 打印出來,如果要處理對應事件。也可在tap這里處理
來看下
CoreText 能做的不止這些希望大家這兩篇文章可以帶大家了解CoreText,不再懼怕使用底層的API。