iOS富文本組件的實現—DTCoreText源碼解析 渲染篇

來源:互聯網
上載者:User

標籤:

本文轉載至 http://blog.cnbang.net/tech/2729/

上一篇介紹了DTCoreText怎樣把HTML+CSS解析轉換成NSAttributeString,本篇接著看看怎樣把NSAttributeString渲染出來。

CoreText

先簡單介紹下CoreText,CoreText是iOS/OSX裡的文字渲染引擎,在iOS/OSX上看到的所有文字在底層都是由CoreText去渲染。

CoreText會把一行裡連在一起相同屬性的文字合在一起作為一個CTRun,每一行是一個CTLine,多行合在一起組成CTFrame。如,第一行的文字有兩種樣式,第一部分是加粗,第二部分是斜體,因為樣式不同所以分成了兩個CTRun,CTLine包含了這兩個CTRun,CTFrame包含了所有CTLine。

一個NSAttributeString可以通過CoreText提供的方法產生CTFramesetter,CTFramesetter是用於建立CTFrame的工廠,給CTFramesetter一個CGPath,或者簡單理解為給他一個框框,它就會通過它持有的CTTypesetter產生CTFrame,CTFrame產生時裡麵包含的CTLine和CTRun就全部產生好了,可以直接繪製到畫布上。CTFrame/CTLine/CTRun都提供了渲染介面,但前兩者是封裝,最後實際都是調用到CTRun的渲染介面去繪製。

如果要用CoreText渲染NSAttributeString,可以簡單產生CTFramesetter,再產生CTFrame,在UIView的drawRect方法裡直接把CTFrame繪製到當前畫布上:

12345678 - (void) drawRect:(CGRect)rect{     UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 320, 400)];     CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)content);     CTFrame frame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0, 0), [path CGPath] , NULL);     CGContextRef ctx = UIGraphicsGetCurrentContext();     CTFrameDraw(frame, ctx);}

CoreText會按NSAttributeString裡的樣式屬性把文字渲染出來。這種是最簡單的粗粒度的渲染方式,但如果需要對文字渲染再做進一步處理,例如添加背景色等這些CoreText沒有支援的屬性,或者要在文字中間插入圖片,就不能簡單繪製CTFrame,需要逐行或逐個CTRun處理。

概覽

DTCoreText需要處理穿插在文字裡的各類Attachment,並支援文字背景色,段縮排等CoreText不支援的屬性,不能簡單把NSAttributeString扔給CoreText渲染了事,需要做更細緻的處理。DTCoreText分了幾層,整體結構圖:

最上層是使用者,可以是Controller,例如項目裡樣本的DemoTextViewController,也可以是某視圖類。接著是DTCoreText封裝好的各個控制項,內建的有Label,TextView和Cell,這些控制項的文字渲染都由DTAttributedTextContentView負責,非文字部分例片/視頻等元素會在上層使用者那裡通過delegate傳給DTAttributedTextContentView。DTCoreTextLayouter / DTCoreTextLayoutFrame / DTCoreTextLayoutLine / DTCoreTextGlyphRun這四個類分別對應CoreText裡的CTFramesetter / CTFrame / CTLine / CTRun,模仿了CoreText的模式,功能和作用一樣,只是在它們基礎上添加了功能。接下來看看每一個類具體做了什麼事情。

DTAttributedTextContentView

DTAttributedTextContentView繼承自UIView,作為DTCoreTextLayoutFrame和上層控制項的中介層,負責按需求繪製內容,大致做了以下幾件事:

1.支援CATiledLayer分段渲染

把UIView的layerClass設為CATiledLayer就能實現分地區渲染,即只渲染顯示在螢幕上的地區,類似那些地圖APP的效果,主要用於像TextView這樣可能內容很長的控制項,避免一次性把全部內容渲染出來,只渲染能看到的部分,提高效能。使用CATiledLayer後,在-drawLayer:inContext:方法裡用CGContextGetClipBoundingBox通過context取得當前顯示的地區,DTCoreTextLayoutFrame只渲染這個地區的內容就行了。

2.產生DTCoreTextLayoutFrame並繪製

通過上層傳進來的NSAttributeString產生DTCoreTextLayouter和DTCoreTextLayoutFrame,進行各種配置後用DTCoreTextLayoutFrame渲染文字到當前layer上,這些配置包括 是否顯示圖片連結/限定行數/斷行規則等。

3.處理Attachment和Link

在-layoutSubviewsInRect:方法裡遍曆DTCoreTextLayoutFrame裡的每一個DTCoreTextGlyphRun,找出有附件和連結的Run進行處理,附件包括圖片/視頻等,建立這些附件對應的view,把這些view按DTCoreTextGlyphRun計算好的位置添加到專門存放附件和連結的customViews上完事。

實際上這些附件view的建立是在上層使用者那裡,DTAttributedTextContentView通過delegate把每個附件的內容和對應的frame傳到上層產生相應的view再給回來,這樣做估計是因為對附件的處理每個使用者的需求都不一樣,不應該直接寫死在底層,例如有些使用者要求圖片需要點擊後放大,視頻需要用自己的控制項等。

DTCoreTextLayouter

DTCoreTextLayouter負責產生和緩衝DTCoreTextLayoutFrame,相當於CTFramesetter和CTFrame的關係,做的事很簡單,就是通過NSAttributeString產生CTFramesetter,再根據不同的rect產生DTCoreTextLayoutFrame,並緩衝這些frame。

DTCoreTextLayoutFrame

DTCoreTextLayoutFrame是最重要的一個類,負責渲染文字,主要做了兩件事:產生行和渲染每一行。

產生DTCoreTextLayoutLine

-_buildLinesWithTypesetter:會建立出當前frame範圍內可見的每一行DTCoreTextLayoutLine,建立過程中做的處理包括:

1.支援整段縮排

從NSAttributeString裡取出當前行是否有表示縮排的DTTextBlock,如果需要縮排,要計算出當前行縮排後的寬度和位置。

2.支援截斷加省略符號

上層像Label/TextView這樣的控制項是限制了寬高的,如果內容超出了寬高,就需要對最後一行進行處理,在合適的位置加”…”。

這裡有個問題,就是必須在渲染到超出寬高的那一行時,才知道要處理的最後一行是什麼。例如一個TextView高40,文字每行高15,在渲染第三行時高已經到45,發現已經超出了TextView的高度,這時知道只能渲染到第二行,但當前已經處理到第三行了,需要把第二行拿出來截斷加”…”。

另外除了超出高度,在超出外部傳進來的numberOfLines時也要截斷,為了統一流程,這裡的做法是在渲染超出高度時記錄總共可以渲染多少行(_numberLinesFitInFrame),然後全部重新來,從頭到尾再產生每一行,這時已經知道總共有多少行,在產生最後一行時處理就行了。這樣做優點是簡單粗暴避免重複代碼,缺點是浪費效能,前面所有行都要重新排一遍。

3.支援hyphen

hyphen是連字號號,就是讓英文單詞在合適的位置換行並加上破折號”-”。CoreText原生不支援hyphen,斷行方式只有按單詞斷行和安字母斷行。這裡hyphen的實現方式是:在所有英文單詞裡可以加破折號的位置全部加上預留位置0x00AD,例如location->lo-ca-tion->lo0x00ADca0x00ADtion。0x00AD是不可見字元,CoreText不會渲染這個字元,但在這個字元的位置是可以斷行的,CoreText不再認為location是一個單詞,會在預留位置處換行。DTCoreText做的處理就是如果發現換行處是預留位置0x00AD,就替換成破折號”-”,所以要支援hyphen,傳進來的內容就必須是所有單詞都寫好預留位置的,否則無效。

4.計算每一行在當前frame的位置

在產生每一行時是不知道這一行在當前frame的位置的,需要自己手動計算。每一行的x座標容易確定,但y座標的計算就要費一番功夫。要考慮的因素有當前行高,上一行位置,行距,段間距,padding,baseline等。

,每一行以baseline為基準,需要計算出這一行的baseline在當前frame的Y座標值,asent與descent是CoreText給出的值,asent+descent就是行高。推算當前行baseline位置的流程是:

  • A.計算上一行的行末位置,即baseline+descent
  • B.計算上一行行間距的一半,例如1.5倍行間距,就是 ((1.5 – 1)*asent+descent)/2
  • C.計算當前行行間距的一半,演算法同上,只是這一行的行間距不一定與上一行一致。這裡兩行各算一半也是為了不同行間距的中和。
  • D.上述計算結果相加,再加上當前行asent值,就得到當前行的baseline Y座標值。

除了上述主流程,還針對首行,段首段尾,DTTextBlock的留白和附件Attachment做了處理,計算的邏輯在-_algorithmWebKit_BaselineOriginToPositionLine。

5.處理對齊

要對每一種對齊進行處理,靠右對齊和置中對齊需要計算出行的x座標值,左右對齊需要通過CTLineCreateJustifiedLine方法重新建立出一個左右對齊的行,針對左右對齊這裡還要了兩件事,一是段末不做左右對齊,二是若內容長度不夠(預設是不足行寬的60%)也不做左右對齊,避免文字間距展開得太厲害效果差。

6.封裝成DTCoreTextLayoutLine

經過上述處理,每一行的CTLine對象以及這一行的位置資訊都有了,把這些封裝成DTCoreTextLayoutLine儲存起來,任務就完成了。

渲染

DTCoreTextLayoutFrame對外提供了-drawInContext:options:方法,用於把上述產生的每一行都渲染到傳進來的context畫布上。做的處理包括:

1.繪製DTTextBlock樣式

DTCoreText支援段落加背景色,在這裡會先找出所有DTTextBlock,通過一系列麻煩的方法取到這些block的座標和大小,把它們對應的背景色畫出來。

2.繪製附件

實現了DTTextAttachmentDrawing介面的附件可以在這裡跟文字一起繪製出來,在DTCoreText裡圖片附件就是實現了DTTextAttachmentDrawing介面,可以直接把圖片在這裡繪製出來。實際片附件的渲染DTCoreText提供了兩種方式,上面介紹DTAttributedTextContentView時說圖片附件也可以在上層讓使用者自行添加,若要在上層自行添加,可以傳參數告訴DTCoreTextLayoutFrame繪製時不要處理圖片附件。

3.繪製文字和陰影

最後就是再遍曆每一行DTCoreTextLayoutLine以及行裡的每一個DTCoreTextGlyphRun,調用它的-drawInContext:方法逐個run繪製到畫布上。繪製時需要算好每個Run的位置,調用CGContextSetTextPosition定位到指定位置繪製文字。繪製文字同時還處理了陰影製作效果,CoreText不直接支援文字陰影製作效果,但可以用CoreGraphic的介面在繪製時加上陰影,這裡還支援同時存在多個shadow -_-!

DTCoreTextLayoutLine

DTCoreTextLayoutLine封裝了CTLine,做的事包括:

1.產生GlyphRun

通過CTLine可以取出所這一行裡的CTRun,計算每個CTRun的位置,封裝產生DTCoreTextGlyphRun。

2.計算屬性和提供輔助方法

計算並儲存了這一行asent/descent/lineHeight等屬性,提供各種輔助方法方便擷取這一行裡的資訊,包括通過stringIndex擷取對應文字的座標等,CTLine相關的幾個方法例如CTLineGetOffsetForStringIndex() / CTLineGetStringIndexForPosition()也有相應的封裝。

DTCoreTextGlyphRun

DTCoreTextGlyphRun裡做的事跟DTCoreTextLayoutLine差不多,只是在渲染方法裡額外做了一些事,首先支援文字背景色,這是CoreText原生不支援的,如果Attribute裡有背景色的屬性,這裡會繪製出來。然後支援iOS6以下文字的底線和刪除線,iOS6以前CoreText是不支援底線和刪除線的,這裡自己做了處理把它畫上去。

總結

整個流程最核心的就是DTCoreTextLayoutFrame產生行和渲染的實現,相當於把CoreText原生的CTFramesetterCreateFrame / CTFrameDraw再自己實現了一遍,在實現的過程加上自己特殊的需求,從中我們也可以大致瞭解到CTFrame/CTLine內部大致實現是怎樣的。CoreText已經提供了足夠細粒度的介面讓使用者可以按自己意願去隨意排版,DTCoreText這一系列的處理給出了很好的樣本可供參考。

iOS富文本組件的實現—DTCoreText源碼解析 渲染篇

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.