<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      iOS/Swift:深入理解iOS CoreText API

      這篇文章是從0到1自定義富文本渲染的原理篇之一,此外你還可能感興趣:

      更多內容可訂閱公眾號「非專業程序員Ping」,文中所有代碼可在公眾號后臺回復 “CoreText” 獲取。

      一、引言

      CoreText是iOS/macOS中的文字排版引擎,提供了一系列對文本精確操作的API;UIKit中UILabel、UITextView等文本組件底層都是基于CoreText的,可以看官方提供的層級圖:

      在這里插入圖片描述
      本文的目的是結合實際使用例子,來介紹和總結CoreText中的重要概念和API。

      二、重要概念

      CoreText中有幾個重要概念:CTTypesetter、CTFramesetter、CTFrame、CTLine、CTRun;它們之間的關系可以看官方提供的層級圖:

      在這里插入圖片描述

      一篇文檔可以分為:文檔 -> 段落 -> 段落中的行 -> 行中的文字,類似的,CoreText也是按這個結構來組織和管理API的,我們也可以根據訴求來選擇不同層級的API。

      2.1 CTFramesetter

      CTFramesetter類似于文檔的概念,它負責將多段文本進行排版,管理多個段落(CTFrame)。

      CTFramesetter的輸入是屬性字符串(NSAttributedString)和路徑(CGPath),負責將文本在指定路徑上進行排版。

      2.2 CTFrame

      CTFrame類似于段落的概念,其中包含了若干行(CTLine)以及對應行的位置、方向、行間距等信息。

      2.3 CTLine

      CTLine類似于行的概念,其中包含了若干個字形(CTRun)以及對應字形的位置等信息。

      2.4 CTRun

      需要注意CTRun不是單個的字符,而是一段連續的且具有相同屬性(字體、顏色等)的字形(Glyph)。

      如下,每個虛線框都代表一個CTRun:

      在這里插入圖片描述

      2.5 CTTypesetter

      CTTypesetter支持對屬性字符串進行換行,可以通過CTTypesetter來自定義換行(比如按word換行、按char換行等)或控制每行的內容,可以理解成更精細化的控制。

      三、重要API

      3.1 CTFramesetter

      1)CTFramesetterCreateWithAttributedString

      func CTFramesetterCreateWithAttributedString(_ attrString: CFAttributedString) -> CTFramesetter
      

      通過屬性字符串來創建CTFramesetter。

      我們可以構造不同字體、顏色、大小的屬性字符串,然后從屬性字符串構造CTFramesetter,之后可以繼續往下拆分得到段落、行、字形等信息,這樣可以實現自定義排版、圖文混排等復雜富文本樣式。

      2)CTFramesetterCreateWithTypesetter

      func CTFramesetterCreateWithTypesetter(_ typesetter: CTTypesetter) -> CTFramesetter
      

      通過CTTypesetter來創建CTFramesetter,當我們需要對文本實現更精細控制,比如自定義換行時,可以自己構造CTTypesetter。

      3)CTFramesetterCreateFrame

      func CTFramesetterCreateFrame(
          _ framesetter: CTFramesetter,
          _ stringRange: CFRange,
          _ path: CGPath,
          _ frameAttributes: CFDictionary?
      ) -> CTFrame
      

      生成CTFrame:在指定路徑(path)為屬性字符串的指定范圍(stringRange)生成CTFrame。

      • framesetter
      • stringRange:字符范圍,注意需要以UTF-16編碼格式計算;當 stringRange.length = 0 時,表示從起點(stringRange.location)到字符結束為止;比如當 CFRangeMake(0, 0) 表示全字符范圍
      • path:排版路徑,可以是不規則矩形,這意味著可以傳入不規則圖形來實現文字環繞等高級效果
      • frameAttributes:一個可選的字典,可以用于控制段落級別的布局行為,比如行間距等,一般用不到,可傳 nil

      4)CTFramesetterSuggestFrameSizeWithConstraints

      func CTFramesetterSuggestFrameSizeWithConstraints(
          _ framesetter: CTFramesetter,
          _ stringRange: CFRange,
          _ frameAttributes: CFDictionary?,
          _ constraints: CGSize,
          _ fitRange: UnsafeMutablePointer<CFRange>?
      ) -> CGSize
      

      計算文本寬高:在給定約束尺寸(constraints)下計算文本范圍(stringRange)的實際寬高。

      如下,我們可以計算出在寬高 100 x 100 的范圍內排版,實際能放下的文本范圍(fitRange)以及實際的文本尺寸:

      let attr = NSAttributedString(string: "這是一段測試文本,通過調用CTFramesetterSuggestFrameSizeWithConstraints來計算文本的寬高信息,并返回實際的range", attributes: [
          .font: UIFont.systemFont(ofSize: 16),
          .foregroundColor: UIColor.black
      ])
      let framesetter = CTFramesetterCreateWithAttributedString(attr)
      var fitRange = CFRange(location: 0, length: 0)
      let size = CTFramesetterSuggestFrameSizeWithConstraints(
          framesetter,
          CFRangeMake(0, 0),
          nil,
          CGSize(width: 100, height: 100),
          &fitRange
      )
      print(size, fitRange, attr.length)
      

      這個API在分頁時非常有用,比如微信讀書的翻頁效果,需要知道在哪個地方截斷,PDF的分頁排版等。

      3.1.1 CTFramesetter使用示例

      1)實現一個支持AutoLayout且高度靠內容撐開的富文本View

      在這里插入圖片描述

      2)在圓形路徑中繪制文本

      在這里插入圖片描述

      3)文本分頁:模擬微信讀書的分頁邏輯
      在這里插入圖片描述

      3.2 CTFrame

      1)CTFramesetterCreateFrame

      func CTFramesetterCreateFrame(
          _ framesetter: CTFramesetter,
          _ stringRange: CFRange,
          _ path: CGPath,
          _ frameAttributes: CFDictionary?
      ) -> CTFrame
      

      創建CTFrame,在CTFramesetter一節中有介紹過,這是創建CTFrame的唯一方式。

      2)CTFrameGetStringRange

      func CTFrameGetStringRange(_ frame: CTFrame) -> CFRange
      

      獲取CTFrame包含的字符范圍。

      我們在調用CTFramesetterCreateFrame創建CTFrame時,會傳入一個 stringRange 的參數,CTFrameGetStringRange也可以理解成獲取這個 stringRange,區別是處理了當 stringRange.length 為0的情況。

      3)CTFrameGetVisibleStringRange

      func CTFrameGetVisibleStringRange(_ frame: CTFrame) -> CFRange
      

      獲取CTFrame實際可見的字符范圍。

      我們在調用CTFramesetterCreateFrame創建CTFrame時,會傳入path,可能會把字符截斷,CTFrameGetVisibleStringRange返回的就是可見的字符范圍。

      需要注意和CTFrameGetStringRange進行區分,可以用如下Demo驗證:

      let longText = String(repeating: "這是一個分欄布局的例子。Core Text 允許我們將一個長的屬性字符串(CFAttributedString)流動到多個不同的路徑(CGPath)中。我們只需要創建一個 CTFramesetter,然后循環調用 CTFramesetterCreateFrame。每次調用后,我們使用 CTFrameGetStringRange 來找出有多少文本被排入了當前的框架,然后將下一個框架的起始索引設置為這個范圍的末尾。 ", count: 10)
      let attributedText = NSAttributedString(string: longText, attributes: [
          .font: UIFont.systemFont(ofSize: 12),
          .foregroundColor: UIColor.darkText
      ])
      let framesetter = CTFramesetterCreateWithAttributedString(attributedText as CFAttributedString)
      let path = CGPath(rect: .init(x: 10, y: 100, width: 400, height: 200), transform: nil)
      let frame = CTFramesetterCreateFrame(
          framesetter,
          CFRange(location: 100, length: 0),
          path,
          nil
      )
      // 輸出:CFRange(location: 100, length: 1980)
      print(CTFrameGetStringRange(frame))
      // 輸出:CFRange(location: 100, length: 584)
      print(CTFrameGetVisibleStringRange(frame))
      

      4)CTFrameGetPath

      func CTFrameGetPath(_ frame: CTFrame) -> CGPath
      

      獲取創建CTFrame時傳入的path。

      5)CTFrameGetLines

      func CTFrameGetLines(_ frame: CTFrame) -> CFArray
      

      獲取CTFrame中所有的行(CTLine)。

      6)CTFrameGetLineOrigins

      func CTFrameGetLineOrigins(
          _ frame: CTFrame,
          _ range: CFRange,
          _ origins: UnsafeMutablePointer<CGPoint>
      )
      

      獲取每一行的起點坐標。

      用法示例:

      let lines = CTFrameGetLines(frame) as! [CTLine]
      var origins = [CGPoint](repeating: .zero, count: lines.count)
      CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
      

      7)CTFrameDraw

      func CTFrameDraw(
          _ frame: CTFrame,
          _ context: CGContext
      )
      

      繪制CTFrame。

      3.2.1 CTFrame使用示例

      1)繪制CTFrame

      在這里插入圖片描述

      2)高亮某一行

      在這里插入圖片描述

      3)檢測點擊字符

      在這里插入圖片描述

      3.3 CTLine

      1)CTLineCreateWithAttributedString

      func CTLineCreateWithAttributedString(_ attrString: CFAttributedString) -> CTLine
      

      從屬性字符串創建單行CTLine,如果字符串中有換行符(\n)的話,換行符會被轉換成空格,如下:

      let line = CTLineCreateWithAttributedString(
          NSAttributedString(string: "Hello CoreText\nWorld", attributes: [.font: UIFont.systemFont(ofSize: 16)])
      )
      

      2)CTLineCreateTruncatedLine

      func CTLineCreateTruncatedLine(
          _ line: CTLine,
          _ width: Double,
          _ truncationType: CTLineTruncationType,
          _ truncationToken: CTLine?
      ) -> CTLine?
      

      創建一個被截斷的新行。

      • line:待截斷的行
      • width:在多少寬度截斷
      • truncationType:start/end/middle,截斷類型
      • truncationToken:在截斷處添加的字符,nil表示不添加,一般使用省略符(...)
      let truncationToken = CTLineCreateWithAttributedString(
          NSAttributedString(string: "…", attributes: [.font: UIFont.systemFont(ofSize: 16)])
      )
      let truncated = CTLineCreateTruncatedLine(line, 100, .end, truncationToken)
      

      3)CTLineCreateJustifiedLine

      func CTLineCreateJustifiedLine(
          _ line: CTLine,
          _ justificationFactor: CGFloat,
          _ justificationWidth: Double
      ) -> CTLine?
      

      創建一個兩端對齊的新行,類似書籍或報紙中兩端對齊的排版效果。

      • line:原始行
      • justificationFactorjustificationFactor <= 0表示不縮放,即與原始行相同;justificationFactor >= 1表示完全縮放到指定寬度;0 < justificationFactor < 1表示部分縮放到指定寬度,可以看示例代碼
      • justificationWidth:縮放指定寬度

      示例:

      在這里插入圖片描述

      4)CTLineDraw

      func CTLineDraw(
          _ line: CTLine,
          _ context: CGContext
      )
      

      繪制行。

      5)CTLineGetGlyphCount

      func CTLineGetGlyphCount(_ line: CTLine) -> CFIndex
      

      獲取行內字形總數。

      6)CTLineGetGlyphRuns

      func CTLineGetGlyphRuns(_ line: CTLine) -> CFArray
      

      獲取行內所有的CTRun。

      7)CTLineGetStringRange

      func CTLineGetStringRange(_ line: CTLine) -> CFRange
      

      獲取該行對應的字符范圍。

      8)CTLineGetPenOffsetForFlush

      func CTLineGetPenOffsetForFlush(
          _ line: CTLine,
          _ flushFactor: CGFloat,
          _ flushWidth: Double
      ) -> Double
      

      獲取在指定寬度繪制時的水平偏移,一般配合 CGContext.textPosition 使用,可用于實現在固定寬度下文本的左對齊、右對齊、居中對齊及自定義水平偏移等。

      示例:

      在這里插入圖片描述

      9)CTLineGetImageBounds

      func CTLineGetImageBounds(
          _ line: CTLine,
          _ context: CGContext?
      ) -> CGRect
      

      獲取行的?視覺邊界?;注意 CTLineGetImageBounds 獲取的是?相對于CTLine局部坐標系的矩形?,即以textPosition為原點的矩形。

      視覺邊界可以看下面的例子,與之相對的是布局邊界;這個API在實際應用中不常見,除非有特殊訴求,比如要檢測精確的內容點擊范圍,給行繪制緊貼背景等。

      在這里插入圖片描述

      10)CTLineGetTypographicBounds

      func CTLineGetTypographicBounds(
          _ line: CTLine,
          _ ascent: UnsafeMutablePointer<CGFloat>?,
          _ descent: UnsafeMutablePointer<CGFloat>?,
          _ leading: UnsafeMutablePointer<CGFloat>?
      ) -> Double
      

      獲取上行(ascent)、下行(descent)、行距(leading)。

      這幾個概念不熟悉的可以參考:一文讀懂字符、字形、字體

      想了解這幾個數值最終是從哪個地方讀取的可以參考:一文讀懂字體文件

      通過這個API我們可以手動構造?布局邊界?(見上面的例子),一般用于點擊檢測、繪制行背景等。

      11)CTLineGetTrailingWhitespaceWidth

      func CTLineGetTrailingWhitespaceWidth(_ line: CTLine) -> Double
      

      獲取行尾空白字符的寬度(比如空格、制表符 (\t) 等),一般用于實現對齊時基于可見文本對齊等。

      示例:

      let line = CTLineCreateWithAttributedString(
          NSAttributedString(string: "Hello  ", attributes: [.font: UIFont.systemFont(ofSize: 16)])
      )
      
      let totalWidth = CTLineGetTypographicBounds(line, nil, nil, nil)
      let trailingWidth = CTLineGetTrailingWhitespaceWidth(line)
      
      print("總寬度: \(totalWidth)")
      print("尾部空白寬度: \(trailingWidth)")
      print("可見文字寬度: \(totalWidth - trailingWidth)")
      

      12)CTLineGetStringIndexForPosition

      func CTLineGetStringIndexForPosition(
          _ line: CTLine,
          _ position: CGPoint
      ) -> CFIndex
      

      獲取給定位置處的字符串索引。

      ?注意:?雖然官方文檔說這個API一般用于點擊檢測,但實際測試下來?這個API返回的點擊索引不準確?,比如雖然點擊的是當前字符,但實際返回的索引是后一個字符的,如下:

      在這里插入圖片描述

      查了下,發現這個API一般是用于計算光標位置的,比如點擊「行」的左半部分,希望光標出現在「行」左側,如果點擊「行」的右半部分,希望光標出現在「行」的右側。

      如果我們想精確做字符的點擊檢測,推薦使用字符/行的bounds來計算,參考「CTFrame使用示例-3」例子。

      13)CTLineGetOffsetForStringIndex

      func CTLineGetOffsetForStringIndex(
          _ line: CTLine,
          _ charIndex: CFIndex,
          _ secondaryOffset: UnsafeMutablePointer<CGFloat>?
      ) -> CGFloat
      

      獲取指定字符索引相對于行的 x 軸偏移量。

      • line:待查詢的行
      • charIndex:要查詢的字符在原始屬性字符串中的索引
      • secondaryOffset:次要偏移值,在簡單的LTR文本中,可以忽略(傳nil即可),但在復雜的雙向文本(BiDi)中會用到

      使用場景:

      • 字符點擊檢測:見「CTFrame使用示例-3」例子
      • 給某段字符繪制高亮和下劃線
      • 定位某個字符:比如想在一段文本中的某個字符上方顯示彈窗,可以用這個API先定位該字符

      14)CTLineEnumerateCaretOffsets

      func CTLineEnumerateCaretOffsets(
          _ line: CTLine,
          _ block: @escaping (Double, CFIndex, Bool, UnsafeMutablePointer<Bool>) -> Void
      )
      

      遍歷一行中光標所有的有效位置。

      • line
      • block
        • Double:offset,相對于行的 x 軸偏移
        • CFIndex:與此光標位置相關的字符串索引
        • Bool:true 表示光標位于字符的前邊(在 LTR 中即左側),false 表示光標位于字符的后邊(在 LTR 中即右側);在 BiDi 中需要特殊同一個字符可能會回調兩次(比如 BiDi 邊界的地方),需要用這個值區分前后
        • UnsafeMutablePointer:stop 指針,賦值為 true 會停止遍歷

      使用場景:

      • 繪制光標:富文本選區或者文本編輯器中,要繪制光標時,可以先通過 CTLineGetStringIndexForPosition 獲取字符索引,再通過這個函數或者 CTLineGetOffsetForStringIndex 獲取光標偏移
      • 實現光標的左右鍵移動:可以用這個API將所有的光標位置存儲到數組,并按offset排序,當用戶按下右箭頭 -> 時,可以找到當前光標index,將index + 1即是下一個光標位置

      3.3.1 CTLine使用示例

      除了上面例子,再舉一個:

      1)高亮特定字符

      在這里插入圖片描述

      3.4 CTRun

      CTRun相關API比較基礎,這里主要介紹常用的。

      1)CTLineGetGlyphRuns

      func CTLineGetGlyphRuns(_ line: CTLine) -> CFArray
      

      獲取CTRun的唯一方式。

      2)CTRunGetAttributes

      func CTRunGetAttributes(_ run: CTRun) -> CFDictionary
      

      獲取CTRun的屬性;比如想知道這個CTRun是不是粗體,是不是鏈接,是不是目標Run等,都可以通過這個API。

      示例:

      guard let attributes = CTRunGetAttributes(run) as? [NSAttributedString.Key: Any] else { continue }
      // 現在你可以檢查屬性
      if let color = attributes[.foregroundColor] as? UIColor {
          // ...
      }
      if let font = attributes[.font] as? UIFont {
          // ...
      }
      if let link = attributes[NSAttributedString.Key("my_custom_link_key")] {
          // 這就是那個可點擊的 run!
      }
      

      3)CTRunGetStringRange

      func CTRunGetStringRange(_ run: CTRun) -> CFRange
      

      獲取CTRun對應于原始屬性字符串的哪個范圍。

      4)CTRunGetTypographicBounds

      func CTRunGetTypographicBounds(
          _ run: CTRun,
          _ range: CFRange,
          _ ascent: UnsafeMutablePointer<CGFloat>?,
          _ descent: UnsafeMutablePointer<CGFloat>?,
          _ leading: UnsafeMutablePointer<CGFloat>?
      ) -> Double
      

      獲取CTRun的度量信息,同上面許多API一樣,當 range.length 為0時表示直到CTRun文本末尾。

      5)CTRunGetPositions

      func CTRunGetPositions(
          _ run: CTRun,
          _ range: CFRange,
          _ buffer: UnsafeMutablePointer<CGPoint>
      )
      

      獲取CTRun中每一個字形的位置,注意這里的位置是相對于CTLine原點的。

      6)CTRunDelegate

      CTRunDelegate允許為屬性字符串中的一段文本提供自定義布局測量信息,一般用于在文本中插入圖片、自定義View等非文本元素。

      比如在文本中間插入圖片:

      在這里插入圖片描述

      3.4.1 CTRun使用示例

      1)基礎繪制

      在這里插入圖片描述

      2)鏈接點擊識別

      在這里插入圖片描述

      3.5 CTTypesetter

      CTFramesetter會自動處理換行,當我們想手動控制換行時,可以用CTTypesetter。

      1)CTTypesetterSuggestLineBreak

      func CTTypesetterSuggestLineBreak(
          _ typesetter: CTTypesetter,
          _ startIndex: CFIndex,
          _ width: Double
      ) -> CFIndex
      

      按單詞(word)換行。

      如下示例,輸出:Try word wrapping

      let attrStringWith = NSAttributedString(string: "Try word wrapping", attributes: [.font: UIFont.systemFont(ofSize: 18)])
      let typesetter = CTTypesetterCreateWithAttributedString(attributedString)
      let totalLength = attributedString.length // UTF-16 長度
      var startIndex = 0
      var lineCount = 1
      
      while startIndex < totalLength {
          let charCount = CTTypesetterSuggestLineBreak(typesetter, startIndex, 100)
          // 如果返回 0,意味著一個字符都放不下(或已結束)
          if charCount == 0 {
              if startIndex < totalLength {
                  print("Line \(lineCount): (Error) 無法放下剩余字符。")
              }
              break
          }
          // 獲取這一行的子字符串
          let range = NSRange(location: startIndex, length: charCount)
          let lineString = (attributedString.string as NSString).substring(with: range)
          print("Line \(lineCount): '\(lineString)' (UTF-16 字符數: \(charCount))")
          // 更新下一次循環的起始索引
          startIndex += charCount
          lineCount += 1
      }
      

      2)CTTypesetterSuggestClusterBreak

      func CTTypesetterSuggestClusterBreak(
          _ typesetter: CTTypesetter,
          _ startIndex: CFIndex,
          _ width: Double
      ) -> CFIndex
      

      按字符(char)換行。

      如下示例,輸出:Try word wrapping

      let attrStringWith = NSAttributedString(string: "Try word wrapping", attributes: [.font: UIFont.systemFont(ofSize: 18)])
      let typesetter = CTTypesetterCreateWithAttributedString(attributedString)
      let totalLength = attributedString.length // UTF-16 長度
      var startIndex = 0
      var lineCount = 1
      
      while startIndex < totalLength {
          let charCount = CTTypesetterSuggestClusterBreak(typesetter, startIndex, 100)
          // 如果返回 0,意味著一個字符都放不下(或已結束)
          if charCount == 0 {
              if startIndex < totalLength {
                  print("Line \(lineCount): (Error) 無法放下剩余字符。")
              }
              break
          }
          // 獲取這一行的子字符串
          let range = NSRange(location: startIndex, length: charCount)
          let lineString = (attributedString.string as NSString).substring(with: range)
          print("Line \(lineCount): '\(lineString)' (UTF-16 字符數: \(charCount))")
          // 更新下一次循環的起始索引
          startIndex += charCount
          lineCount += 1
      }
      

      四、總結

      以上是CoreText中常用的API及其場景代碼舉例,完整示例代碼可在公眾號「非專業程序員Ping」回復 “CoreText” 獲取。

      posted on 2025-10-19 21:42  非專業程序員Ping  閱讀(31)  評論(0)    收藏  舉報

      導航

      主站蜘蛛池模板: 亚洲偷自拍国综合| 国产精品成人午夜福利| 国内极度色诱视频网站| 国产精品久久久久久av| 国产成人高清亚洲综合| 亚洲天堂av日韩精品| 久久精品国产国产精品四凭| 久久99精品久久久久久9| аⅴ天堂中文在线网| 三人成全免费观看电视剧高清| 国产成人午夜福利在线小电影| 国产av最新一区二区| 亚洲欧美一区二区成人片| 国产乱妇无乱码大黄aa片| 天天澡日日澡狠狠欧美老妇| 秋霞av鲁丝片一区二区| 九九成人免费视频| 九九色这里只有精品国产| 色欲综合久久中文字幕网| 香港经典a毛片免费观看播放| 玩弄放荡人妻少妇系列| 国产精品综合色区av| 免费乱理伦片在线观看| 色偷偷女人的天堂亚洲网| 亚洲一二三区精品与老人| 一区二区三区鲁丝不卡| 在线天堂中文新版www| 国产制服丝袜无码视频| 精品无码国产自产拍在线观看蜜| 中文天堂资源| 精品无码成人片一区二区| 久久99精品久久久久久| 二区中文字幕在线观看| 亚洲中文字幕一区二区| 97夜夜澡人人爽人人模人人喊| 亚洲中文字幕国产精品| 国产综合色在线精品| 久久精品国产一区二区三| 精品国产AⅤ无码一区二区 | 三级国产在线观看| 四虎国产精品免费久久|