Swift: 基于 CoreText 圖文排版實(shí)踐

介紹 CoreText 簡(jiǎn)單應(yīng)用,主要包括文本節(jié)選,可點(diǎn)鏈接,圖文混排等內(nèi)容。

CoreText

CoreText 是用于處理文字和字體的底層技術(shù)。它直接和 Core Graphics 交互。

Coretext

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))
  
屏幕快照 2018-01-09 下午5.12.30.png

在繪制過(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)用。

UIKit & CoreText

文本

直接看代碼吧,簡(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)

}

文本節(jié)選

可點(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

  • 遍歷 resultslinkAttributedString 替換 attributedString 中鏈接地址

  • 記錄 linkAttributedStringrangeurl 得到 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)擊位置 pointcoreTextLinkData 拿到 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è) LineCTRun

  • 通過(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í)際需求的圖文混合

0

繪制的方式是和上面的是一樣的,不同在于圖片和文本的排版不一樣。

實(shí)踐步驟:

0.不做處理

1

1.文字都堆在一起了,給文本不同樣式劃分段落

2

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ì),減少不必要的圖層。

3

但是呢,這只是當(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)

4-Path

可以看到整個(gè)繪制區(qū)域被分成了兩個(gè)部分,一個(gè)是圖片一個(gè)是文本。通過(guò) UIBezierPath 還可以繪制任何想要的形狀。剩下的問(wèn)題就是處理段落之間的行間距。

5

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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評(píng)論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,018評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,675評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,417評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,783評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,960評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,267評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,471評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,698評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,099評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,386評(píng)論 1 294
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,204評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,436評(píng)論 2 378