輕量富文字非同步繪製框架

syik發表於2018-06-28

前言

輕量富文字非同步繪製框架

如果遇到上面一個需求, 你會怎麼處理, 若干個 UILabel + UIImageView? NSAttributedString拼接? CoreText?

我相信不論是哪種方式程式碼量都不小, 並且難以複用, 其他語言寫富文字是那麼輕鬆, Android 天生支援簡單 HTML, RN(JS) 標籤套標籤, 而只要用過 iOS 中的富文字都會覺得難用... 目前業界功能強大、較為好用的是 YYText, 但設計思想是儘可能與 UILabel、UITextView 相似, 所以相對使用也不是特別簡單, 而且框架較重. 基於現狀開發了一套輕量的框架, 滿足大部分富文字需求, 並且提供了手勢響應、 繪製回撥、 圖文對齊、 CoreText 屬性擴充套件、 支援網路圖片、 非同步繪製效能優化, 最重要的是使用簡單, 通過鏈式語法輕鬆寫出一篇圖文混排文字.

示例說明

如圖所示一片圖文混排, 涉及到字型, 顏色, 字間距, 行間距, 圖片對齊, 文字對齊, 描邊等等屬性, 還有網路圖片與本地圖片混排, 手勢響應等需求, 使用本框架可以下面這樣實現:

//...省略常量宣告

TextBuild
.append(title).font(titleFont).color(titleColor).onClicked(titleOnClicked).onLayout(titleOnLayout)
.append(firstPara).color(firstParaColor).align(@0)
.append(webImage).font(separateLineFont).minLineHeight(@100)
.append(separateLine).font(separateLineFont).strokeColor(separateLineColor).strokeWidth(@1).horizontalOffset(@30)
.append(locolImage).horizontalOffset(@30)
.append(lastPara).font(lastParaFont).align(@1).maxLineHeight(@20)
.append(bookName).font(bookNameFont).color(bookNameColor).onClicked(bookOnClicked).align(@1)
.append(lineLayer).attachSize(lineLayerSize)
.append(quote).color(quoteColor).letterSpace(@0).minLineSpace(@8).align(@0)
.append(buyButton).attachSize(buyButtonSize).attachAlign(@0)
//設定全域性預設屬性, 優先順序低於指定屬性
.entire().maxSize(maxSize).align(@2).letterSpace(@3).minLineHeight(@20).attachAlign(@1).onClicked(textOnClicked).attachSize(attachSize).shadow(shadow).cornerRadius(@50).backgroundLayer(gradientLayer).horizontalMargin(@10).preferHeight(@(preferHeight))
//繪製View
.drawView(^(UIView *drawView) {
    [self.view addSubview:drawView];
});
複製程式碼

而在實際需求中也可以根據不同條件對 NSString 進行組合, 最後繪製:

//...省略常量宣告

//拼接文章
//標題
NSString *titleString = TextBuild.append(title).font(titleFont).color(titleColor).onClicked(titleOnClicked).onLayout(titleOnLayout);
//首段
NSString *firstParaString = TextBuild.append(firstPara).color(firstParaColor).align(@0);
//圖片需要用一個空字串起頭
NSString *webImageString = TextBuild.append(webImage).font(separateLineFont).minLineHeight(@100);
//分割線
NSString *separateLineString = TextBuild.append(separateLine).font(separateLineFont).strokeColor(separateLineColor).strokeWidth(@1).horizontalOffset(@30);
//本地圖片
NSString *locolImageString = TextBuild.append(locolImage).horizontalOffset(@30);
//最後一段
NSString *lastParaString = TextBuild.append(lastPara).font(lastParaFont).align(@1).maxLineHeight(@20);
//書名
NSString *bookNameString = TextBuild.append(bookName).font(bookNameFont).color(bookNameColor).onClicked(bookOnClicked).align(@1).maxLineHeight(@20);
//引用線Layer
NSString *lineLayerString = TextBuild.append(lineLayer).attachSize(lineLayerSize);
//引用
NSString *quoteString = TextBuild.append(quote).color(quoteColor).letterSpace(@0).minLineSpace(@8).align(@0);
//按鈕
NSString *buttonString = TextBuild.append(buyButton).attachSize(buyButtonSize).attachAlign(@0);

//設定全域性預設屬性, 優先順序低於指定屬性
NSString *defaultAttributes = TextBuild.entire()
.maxSize(maxSize).align(@2).letterSpace(@3).minLineHeight(@20).attachAlign(@1).onClicked(textOnClicked).attachSize(attachSize).shadow(shadow).cornerRadius(@50).backgroundLayer(gradientLayer).horizontalMargin(@10).preferHeight(@(preferHeight));

//拼接
TextBuild
.append(titleString)
.append(firstParaString)
.append(webImageString)
.append(separateLineString)
.append(locolImageString)
.append(lastParaString)
.append(bookNameString)
.append(lineLayerString)
.append(quoteString)
.append(buttonString)
//設定預設屬性
.append(defaultAttributes)
//繪製Layer
.drawLayer(^(CALayer *drawLayer) {
    [self.view.layer addSublayer:drawLayer];
});
複製程式碼

核心方法與屬性

對 NSString 的擴充套件, 操作字串生成繪製對應檢視

核心方法

  • append(id content)

      拼接
      content 可以是文字(NSString)、圖片(UIImage)、圖片連結(NSURL)(必須指定attachSize屬性)、檢視(CALayer/UIView)
    複製程式碼
  • entire()

      設定整段富文字
      優先順序低於指定屬性, 較為重要的屬性 maxSize 設定繪製約束, 部分段落屬性只在整段中設定生效
    複製程式碼
  • drawLayer(^(CALayer *drawLayer)completion)

      繪製layer, 無法響應手勢, 當有UIView混排建議使用 drawView
    複製程式碼
  • drawView(^(UIView *drawView)completion)

      繪製View, 可響應手勢, 建議使用此API
    複製程式碼

屬性

通用屬性
  • verticalOffset 垂直偏移
  • horizontalOffset 水平方向偏移
  • onClicked 點選回撥
  • onLayout 展示回撥
  • cacheFrame 快取該段文字繪製位置
  • minLineSpace 最小行間距
  • maxLineSpace 最大行間距
  • minLineHeight 最小行高
  • maxLineHeight 最小行高
  • align 對齊, 整形, 0為預設靠左 1為靠右 2為居中, 參考 CTTextAlignment
  • lineBreakMode 對齊, 整形, 參考 NSLineBreakMode
字串屬性
  • font 字型: 文字字型/圖片居中對齊字型
  • color 顏色
  • letterSpace 字間距
  • strokeWidth 描邊寬度, 整數為鏤空, Color不生效; 負數Color生效
  • strokeColor 描邊顏色
  • verticalForm 文字繪製隨文字書寫方向, 預設 否(0), 是(非0)
  • underline 下劃線型別, 整形, 0為none, 1為細線 2為加粗 9為雙條 參考 CTUnderlineStyle(僅列舉了三種, 其他值也有不同效果)
圖片屬性
  • attachSize 圖片尺寸, 預設為圖片本身尺寸, 會根據圖片縮放(2x 3x)自動調整
  • attachAlign 圖片對齊模式, 0為預設, 基準線對齊. 1為居中對齊至特定字型大小 參看 ZJTextattachAlign
段落屬性
注: 下面屬性多段文字拼接只在 entire() 函式後生效; 若只有一段有效文字(非空字元及其他型別), 也可以直接生效.
  • maxSize 繪製的約束尺寸, 預設不限制
  • shadow 文字陰影, 對全文生效
  • preferHeight 期望繪製高度, 內容居中
  • verticalMargin 垂直方向間距, 若設定了 preferHeight 此屬性不生效
  • horizontalMargin 水平方向間距
  • backgroundColor 背景顏色
  • backgroundLayer 背景檢視, 常用圖片背景/漸變色背景
  • cornerRadius 圓角

效能

總體採用 CoreText + 非同步繪製圖片完成, 理論上效能會比較高, 經過測試如下資料供參考:

內容: 一段文字加上兩張圖片

機型: iPhone 6

測試結果:

常規(使用NSAttributedString + UILabel)過程: 建立->顯示(繪製) 常規分析:

  1. 主執行緒程式碼在 28ms 左右. (主執行緒程式碼開始 至 結束耗時)
  2. UILabel 顯示(繪製)耗時在 42ms 左右. (addSubview 至 drawRect 耗時)
  3. 綜合耗時 70ms 左右, 全部在主執行緒

非同步繪製(本框架)過程: 建立->非同步繪製->顯示 非同步繪製分析:

  1. 主執行緒(建立)程式碼在 28ms 左右. (主執行緒程式碼開始 至 結束耗時)
  2. 建立(主執行緒) + 非同步繪製耗時 84ms 左右. (主執行緒程式碼開始 至 繪製出圖片回撥)
  3. 由 1、2 得出子執行緒繪製耗時 56ms 左右, 另外經過多次試驗(大段文字繪製)得出繪製複雜的段落也耗時增長較少
  4. 顯示耗時 0.75 ms 左右. (addSubview 至 drawRect 耗時)
  5. 綜合耗時 85ms 左右, 其中主執行緒 29ms, 子執行緒 56ms

結論:

  1. 相較於常規方式降低了主執行緒壓力 70ms -> 29ms
  2. 越複雜的文字收益越高(多控制元件合一, 非同步繪製), 上圖中大段富文字繪製時間也只多了 15ms, 耗時增長少
  3. 總體耗時增加了15ms, 都在子執行緒, 畢竟處理的邏輯比系統的多.
  4. 總體效能與 YYText 相仿

安裝

Github

ZJAttributedText

Pod

pod 'ZJAttributedText'
複製程式碼

本框架依賴 SDWebImage (幾乎所有App都整合了, 可以共用一套快取邏輯)

尾巴

內部實現程式碼不多, 幾乎所有步驟都新增了註釋, 如果需要學習 CoreText, 非同步繪製, 鏈式語法, 還算是個不錯的 Demo, 如果大家感興趣, 可以補充下 CoreText 相關內容, 這部分網上的資料都比較老, 錯誤也比較多. 歡迎 issue 與 star~

相關文章