iOS上文本处理之简史
iOS 文字簡(jiǎn)史
- iPhone OS 2
- UILabel
- UITextField
- UITextView
- iPhone OS 3
- New Feature: 復(fù)制 && 粘貼
- iOS 3.2
- CoreText
- iOS 4
- None
- iOS 5
- None
- iOS 6
- UILabel 支持 NSAttributedString
- UITextView 支持 NSAttributedString
- iOS 7
- TextKit
TextKit 出現(xiàn)前
Q: 想繪制一段話,這段話需要有不同的字體,可以調(diào)節(jié)行高,可以調(diào)節(jié)字間距,可以做各種富文本編輯工作,可以高亮某些特殊字
A1: 使用很多的 Label 來(lái)完成復(fù)雜的排布,顯然這是最 Ugly 的方法。
A2: 使用 NSAttributedString 來(lái)自定義文字樣式,然后塞給 UILabel 和 UITextView 完成繪制工作,問(wèn)題是渲染邏輯和排布邏輯部分被完全封裝在 UILabel 和 UITextView 中,我們無(wú)法干涉。
A3: 使用 CoreText 來(lái)自己繪制文字,代碼冗余,都是 C 代碼,需要自己做上層面向?qū)ο蠓庋b才便于重用。
PS: Cocoa 和 CocoaTouch 框架中,以 Core 開(kāi)頭的 framework 基本都是 C 語(yǔ)言的底層封裝,如:CoreFoundation, CoreText,也有例外,如:CoreAnimation
TextKit 出現(xiàn)后
完成上面的需求變得更為簡(jiǎn)潔,這個(gè)我們需要先看下 TextKit 的結(jié)構(gòu)。
NSTextStorage[New]----->NSString|NSLayoutManager[New]--->CoreText|NSTextContainer[New]|UITextView------------>UITextInput
UIKit 文本系統(tǒng)新增的三個(gè)類 NSTextStorage, NSLayoutManager, NSTextContainer 分別對(duì)應(yīng)了 MVC[1] 中的不同角色。
Demo
TextKitDemo GitHub 地址
Demo 實(shí)現(xiàn)了
- 特殊字符高亮顯示
- 首行縮進(jìn)
- 行間距設(shè)置
- 段間距設(shè)置
=============
認(rèn)識(shí) TextKit
http://blog.jobbole.com/51965/
iOS7 的發(fā)布給開(kāi)發(fā)者的案頭帶來(lái)了很多新工具。其中一個(gè)就是 TextKit(文本工具箱)。TextKit 由許多新的 UIKit 類組成,顧名思義,這些類就是用來(lái)處理文本的。在這里,我們將介紹 TextKit 的來(lái)由、它的組成,以及通過(guò)幾個(gè)例子解釋開(kāi)發(fā)者怎樣將它派上大用場(chǎng)。
但是首先我們得有一點(diǎn)背景知識(shí):TextKit 可能是近期對(duì) UIKit 最重要的補(bǔ)充了。iOS7 的新界面用純文本按鈕替換了大量的圖標(biāo)和邊框。總的來(lái)說(shuō),文本和文本布局在新的操作系統(tǒng)的外觀方面比以前重要多了。iOS7 的重新設(shè)計(jì)完全是被文本驅(qū)動(dòng),這樣說(shuō)也許并不夸張——而文本全部是TextKit來(lái)處理的。
告訴你這個(gè)變動(dòng)到底有多大吧:iOS7 之前的所有版本,(幾乎)所有的文本都是 WebKit 來(lái)處理的。對(duì):WebKit,web 瀏覽器引擎。所有UILabel、UITextField,以及 UITextView 都在后臺(tái)以某種方式使用 web 視圖來(lái)進(jìn)行文本布局和渲染。為了新的界面風(fēng)格,它們?nèi)急恢匦略O(shè)計(jì)以使用TextKit。
iOS上文本的簡(jiǎn)短歷史
這些新類并不是用來(lái)替換開(kāi)發(fā)者以前使用的類。對(duì) SDK 來(lái)說(shuō),TextKit 提供的是全新的功能。iOS7 之前,TextKit 提供的功能必須都手動(dòng)完成。這是現(xiàn)有功能之間缺失的環(huán)節(jié)。
長(zhǎng)期以來(lái),只有一個(gè)基本的文本布局和渲染框架:CoreText。也有一個(gè)途徑讀取用戶的鍵盤輸入:UITextInput 協(xié)議。iOS6 甚至有一個(gè)途徑來(lái)簡(jiǎn)單地獲取系統(tǒng)的文本選擇:繼承 UITextView。
(這可能是重點(diǎn),我應(yīng)該公開(kāi)我開(kāi)發(fā)文本編輯器的十年經(jīng)驗(yàn)了)在渲染文本和讀取鍵盤輸入之間存在著巨大(跟我讀:巨大)的缺口。這個(gè)缺口可能也是導(dǎo)致很少有富文本或者語(yǔ)法高亮編輯器的原因了——毫無(wú)疑問(wèn),開(kāi)發(fā)一個(gè)好用的文本編輯器得耗費(fèi)幾個(gè)月的時(shí)間。
就這樣——如下是 iOS 文本(不那么)簡(jiǎn)短歷史的簡(jiǎn)短概要:
iOS 2:這是第一個(gè)公開(kāi)的 SDK,包括一個(gè)簡(jiǎn)單的文本顯示組件( UILabel ),一個(gè)簡(jiǎn)單的文本輸入組件( UITextField ),以及一個(gè)簡(jiǎn)單的、可滾動(dòng)、可編輯的并且支持更大量文本的組件:UITextView。這些組件都只支持純文本,沒(méi)有文本選擇支持(僅支持插入點(diǎn)),除了設(shè)置字體和文本顏色外幾乎沒(méi)有其他可定制功能。
iOS 3:新特性有復(fù)制和粘貼,以及復(fù)制粘貼所需要的文本選擇功能。數(shù)據(jù)探測(cè)器(Data Detector)為文本視圖提供了一個(gè)高亮電話號(hào)碼和鏈接的方法。然而,除了打開(kāi)或關(guān)閉這些特性外,開(kāi)發(fā)者基本上沒(méi)有什么別的事情可以做。
iOS 3.2:iPad 的出現(xiàn)帶來(lái)了 CoreText,也就是前面提到的低級(jí)文本布局和渲染引擎(從Mac OS X 10.5 移植過(guò)來(lái)的),以及 UITextInput,前面也提到的鍵盤存取協(xié)議。Apple 將 Pages 作為移動(dòng)設(shè)備上文本編輯功能的樣板工程(附注1)。然而,由于我前面提到的框架缺口,只有很少的應(yīng)用使用它們。
iOS 4:iOS 3.2 發(fā)布僅僅幾個(gè)月后就發(fā)布了,文本方面沒(méi)有一丁點(diǎn)新功能。(個(gè)人經(jīng)歷:在 WWDC,我走近工程師們,告訴他們我想要一個(gè)完善的 iOS 文本布局系統(tǒng)。回答是:“哦…提交個(gè)請(qǐng)求。”不出所料…)
iOS 5:文本方面沒(méi)啥變化。(個(gè)人經(jīng)歷:在 WWDC,我和工程師們談及 iOS 上文本系統(tǒng)。回答是:“我們沒(méi)有看到太多的請(qǐng)求…” 靠!)
iOS 6:有些動(dòng)作了:屬性文本編輯被加入了UITextView。很不幸的是,它很難定制。默認(rèn)的UI有粗體、斜體和下劃線。用戶可以設(shè)置字體大小和顏色。粗看起來(lái)相當(dāng)不錯(cuò),但還是沒(méi)法控制布局或者提供一個(gè)便利的途徑來(lái)定制文本屬性。然而對(duì)于(文本編輯)開(kāi)發(fā)者,有一個(gè)大的新功能:可以繼承 UITextView 了,這樣的話,除了以前版本提供的鍵盤輸入外,開(kāi)發(fā)者可以“免費(fèi)”獲得文本選擇功能。必須實(shí)現(xiàn)一個(gè)完全自定義的文本選擇功能,可能是很多對(duì)非純文本工具開(kāi)發(fā)的嘗試半途而廢的原因。(個(gè)人經(jīng)歷:我,WWDC,工程師們。我想要一個(gè) iOS 的文本系統(tǒng)。回答:“嗯。吖。是的。也許?看,它只是不執(zhí)行…” 所以畢竟還是有希望,對(duì)吧?)
iOS 7:終于來(lái)了,TextKit。
功能
所以咱們到了。iOS7 帶著 TextKit 登陸了。咱們看看它可以做什么!深入之前,我還想提一下,嚴(yán)格來(lái)說(shuō),這些事情中的大部分以前都可以做。如果你有大量的資源和時(shí)間來(lái)用CoreText構(gòu)建一個(gè)文本引擎,這些都是可以做的。但是如果以前你想構(gòu)建一個(gè)完善的富文本編輯器,你得花費(fèi)幾個(gè)月的時(shí)間。現(xiàn)在就非常簡(jiǎn)單,你只需要到在Xcode里打開(kāi)一個(gè)界面文件,然后將UITextView拖到你的試圖控制器,就可以獲得所有的功能:
字距調(diào)整(Kerning):所有的字符都有簡(jiǎn)單的二次的形狀,這些形狀必須被精確地放置,彼此相鄰的,別這樣想了。例如,現(xiàn)代文本布局會(huì)考慮到一個(gè)大寫的“T”的“兩翼”下面有一些空白,所以它會(huì)把后面的小寫字母向左移讓它們更靠近點(diǎn)。從而大大提高了文本的易讀性,特別是在更長(zhǎng)的文字中:
連寫:我認(rèn)為這主要是個(gè)藝術(shù)功能,但當(dāng)某些字符組合(如“f”后面是“l(fā)”)使用組合符號(hào)(所謂的字形(glyph))繪制時(shí),有些文本確實(shí)看起來(lái)更好(更美觀)。
圖像附件:現(xiàn)在可以在文本視圖里面添加圖像了。
斷字:編輯文本時(shí)沒(méi)那么重要,但如果要以好看易讀的方式展現(xiàn)文本時(shí),這就相當(dāng)重要。斷字意味著在行邊界處分割單詞,從而為整體文本創(chuàng)建一個(gè)更整齊的排版和外觀。個(gè)人經(jīng)歷:iOS7 之前,開(kāi)發(fā)者必須直接使用 CoreText。像這樣:首先以句子為基礎(chǔ)檢測(cè)文本語(yǔ)言,然后獲取句子中每個(gè)單詞可能的斷字點(diǎn),然后在每一個(gè)可能的斷字點(diǎn)上插入定制的連字占位字符。準(zhǔn)備好之后,運(yùn)行 CoreText 的布局方法并手動(dòng)將連字符插入到斷行。如果你想得到好的效果,之后你得檢查帶有連字符的文本沒(méi)有超出行邊界,如果超出了,在運(yùn)行一次行的布局方法,這一次不要使用上次使用的斷字點(diǎn)。使用 TextKit 的話,就非常簡(jiǎn)單了,設(shè)置 hyphenationFactor 屬性就可以啟用斷字。
可定制性:對(duì)我來(lái)說(shuō),甚至比改進(jìn)過(guò)的排版還多,這是個(gè)新的功能。以前開(kāi)發(fā)者必須在使用現(xiàn)有的功能和自己全部重頭寫之間做出選擇。現(xiàn)在提供了一整套類,它們有代理協(xié)議,或者可以被覆蓋從而改變部分行為。例如,不必重寫整個(gè)文本組件,你現(xiàn)在就可以改變指定單詞的斷行行為。我認(rèn)為這是個(gè)勝利。
更多的富文本屬性:現(xiàn)在可以設(shè)置不同的下劃線樣式(雙線、粗線、虛線、點(diǎn)線,或者它們的組合)。提高文本的基線非常容易,這可用來(lái)設(shè)置上標(biāo)數(shù)字。開(kāi)發(fā)者也不再需要自己為定制渲染的文本繪制背景顏色了(CoreText 不支持這些功能)。
序列化:過(guò)去沒(méi)有內(nèi)置的方法從磁盤讀取帶文本屬性的字符串。或者再寫回磁盤。現(xiàn)在有了。
文本樣式:iOS7 的界面引入了一個(gè)全局預(yù)定義的文本類型的新概念。這些文本類型分配了一個(gè)全局預(yù)定義的外觀。理想情況下,這可以讓整個(gè)系統(tǒng)的標(biāo)題和連續(xù)文本具有一致的風(fēng)格。通過(guò)設(shè)置應(yīng)用,用戶可以定義他們的閱讀習(xí)慣(例如文本大小),那些使用文本樣式的應(yīng)用將自動(dòng)擁有正確的文本大小和外觀。
文本效果:最后也是最不重要的。iOS7 有且僅有一個(gè)文本效果:凸版。使用此效果的文本看起來(lái)像是蓋在紙上面一樣。內(nèi)陰影,等等。個(gè)人觀點(diǎn):真的?靠…?在一個(gè)已經(jīng)完全徹底不可饒恕地槍斃了所有無(wú)用的懷舊裝飾的操作系統(tǒng)上,誰(shuí)會(huì)需要這個(gè)像文本蓋在紙上的外觀?
結(jié)構(gòu)
可能概覽一個(gè)系統(tǒng)最好的方法是畫一幅圖。這是UIKit文本系統(tǒng)——TextKit的簡(jiǎn)圖,:
從上圖可以看出來(lái),要讓一個(gè)文本引擎工作,需要幾個(gè)參與者。我們將從外到里介紹它們:
字符串(String):要繪制文本,那么必然在某個(gè)地方有個(gè)字符串存儲(chǔ)它。在默認(rèn)的結(jié)構(gòu)中,NSTextStorage 保存并管理這個(gè)字符串,在這種情況中,它可以遠(yuǎn)離繪制。但并不一定非得這樣。使用 TextKit 時(shí),文本可以來(lái)自任何適合的來(lái)源。例如,對(duì)于一個(gè)代碼編輯器,字符串可以是一棵包含所有顯示的代碼的結(jié)構(gòu)信息的注釋語(yǔ)法樹(annotated syntax tree, AST)。使用一個(gè)定制的文本存儲(chǔ),這個(gè)文本只在后面動(dòng)態(tài)地添加字體或顏色高亮等文本屬性裝飾。這是第一次,開(kāi)發(fā)者可以直接為文本組件使用自己的模型。只需要一個(gè)特別設(shè)計(jì)的文本存儲(chǔ)。即:
NSTextStorage:如果你把文本系統(tǒng)看做一個(gè)模型-視圖-控制器(MVC)架構(gòu),這個(gè)類代表的是模型。文本存儲(chǔ)是中心對(duì)象,它知道所有的文本和屬性信息。它只提供了兩個(gè)存取器方法存取它們,并提供了另外兩個(gè)方法來(lái)修改它們。后面我們將進(jìn)一步了解它們。現(xiàn)在重要的是你得理解 NSTextStorage 是從它的父類 NSAttributedString 繼承了這些方法。這就很清楚了,文本存儲(chǔ)——從文本系統(tǒng)看來(lái)——僅僅是一個(gè)帶有屬性的字符串,以及幾個(gè)擴(kuò)展。這兩者唯一的重大不同點(diǎn)是文本存儲(chǔ)包含了一個(gè)方法來(lái)發(fā)送內(nèi)容改變的通知。我們會(huì)馬上介紹這部分內(nèi)容。
UITextView:堆棧的另一頭是實(shí)際的視圖。在 TextKit 中,文本視圖有兩個(gè)目的:第一,它是文本系統(tǒng)用來(lái)繪制的視圖。文本視圖它自己并不會(huì)做任何繪制;它僅僅提供一個(gè)供其它類繪制的區(qū)域。作為視圖層級(jí)機(jī)構(gòu)中唯一的組件,第二個(gè)目的是處理所有的用戶交互。具體來(lái)說(shuō),文本視圖實(shí)現(xiàn) UITextInput 的協(xié)議來(lái)處理鍵盤事件,它為用戶提供了一種途徑來(lái)設(shè)置一個(gè)插入點(diǎn)或選擇文本。它并不對(duì)文本做任何實(shí)際上的改變,僅僅將這些改變請(qǐng)求轉(zhuǎn)發(fā)給剛剛討論的文本存儲(chǔ)。
NSTextContainer:每個(gè)文本視圖定義了一個(gè)文本可以繪制的區(qū)域。為此,每個(gè)文本視圖都有一個(gè)文本容器,它精確地描述了這個(gè)可用的區(qū)域。在簡(jiǎn)單的情況下,這是一個(gè)垂直的無(wú)限相當(dāng)大的矩形區(qū)域。文本被填充到這個(gè)區(qū)域,并且文本視圖允許用戶滾動(dòng)它。然而,在更高級(jí)的情況下,這個(gè)區(qū)域可能是一個(gè)無(wú)限大的矩形。例如,當(dāng)渲染一本書時(shí),每一頁(yè)都有最大的高度和寬度。文本容器會(huì)定義這個(gè)大小,并且不接受任何超出的文本。相同情況下,一幅圖像可能占據(jù)了頁(yè)面的一部分,文本應(yīng)該沿著它的邊緣重新排版。這也是由文本容器來(lái)處理的,我們會(huì)在后面的例子中看到這一點(diǎn)。
NSLayoutManager:布局管理器是中心組件,它把所有組件粘合在一起:
- 1、這個(gè)管理器監(jiān)聽(tīng)文本存儲(chǔ)中文本或?qū)傩愿淖兊耐ㄖ?#xff0c;一旦接收到通知就觸發(fā)布局進(jìn)程。
- 2、從文本存儲(chǔ)提供的文本開(kāi)始,它將所有的字符翻譯為字形(Glyph)(附注2).
- 3、一旦字形全部生成,這個(gè)管理器向它的文本容器(們)查詢文本可用以繪制的區(qū)域
- 4、然后這些區(qū)域被行逐步填充,而行又被字形逐步填充。一旦一行填充完畢,下一行開(kāi)始填充。
- 5、對(duì)于每一行,布局管理器必須考慮斷行行為(放不下的單詞必須移到下一行)、連字符、內(nèi)聯(lián)的圖像附件等等。
- 6、當(dāng)布局完成,文本的當(dāng)前顯示狀態(tài)被設(shè)為無(wú)效,然后文本管理器將前面幾步排版好的文本設(shè)給文本視圖。
CoreText:沒(méi)有直接包含在 TextKit 中,CoreText 是進(jìn)行實(shí)際排版的庫(kù)。對(duì)于布局管理器的每一步,CoreText 被這樣或那樣的方式調(diào)用。它提供了從字符到字形的翻譯,用它們來(lái)填充行,以及建議斷字點(diǎn)。
Cocoa 文本系統(tǒng)
創(chuàng)建像 TextKit 這樣龐大復(fù)雜的系統(tǒng)肯定不是件簡(jiǎn)單快速的事情,而且肯定需要豐富的經(jīng)驗(yàn)和知識(shí)。在 iOS 的前面6個(gè)主版本中,一直沒(méi)有提供一個(gè)“真正的”文本組件,這也說(shuō)明了這一點(diǎn)。Apple 把它視為一個(gè)大的新特性,當(dāng)然沒(méi)啥問(wèn)題。但是它真的是全新的嗎?
這里有個(gè)數(shù)字:在 UIKit 的 131 個(gè)公共類中,只有 9 個(gè)的名字沒(méi)有使用UI作為前綴。這 9 個(gè)類使用的是舊系統(tǒng)的的、舊世界的(跟我讀:Mac OS)前綴 NS。而且這九個(gè)類里面,有七個(gè)是用來(lái)處理文本的。巧合?好吧…
這是 Cocoa 文本系統(tǒng)的簡(jiǎn)圖。不妨和上面 TextKit 的那幅圖作一下對(duì)比。
驚人地相似。很明顯,最起碼主要部分,兩者是相同的。很明顯——除了右邊部分以及 NSTextView 和 UITextView ——主要的類全部相同。TextKit 是(起碼部分是)從 Cocoa 文本系統(tǒng)移植到 iOS。(我之前一直請(qǐng)求的那個(gè),耶!)
進(jìn)一步比較還是能看出一些不同的。最值得注意的有:
在 iOS 上沒(méi)有 NSTypesetter 和 NSGlyphGenerator 這兩個(gè)類。在 Mac OS 上有很多方法來(lái)定制排版,這被極大地簡(jiǎn)化了。這可以去掉一些抽象概念,并將這個(gè)過(guò)程合并到 NSLayoutManager 中來(lái)。保留下來(lái)的是少數(shù)的代理方法,以用來(lái)更改文本布局和斷行行為。
這些類的 iOS 實(shí)現(xiàn)提供了幾個(gè)新的而且非常便利的功能。在 Cocoa 中,必須手工地將確定的區(qū)域從文本容器分離出來(lái)(見(jiàn)上)。而 UIKit 類提供了一個(gè)簡(jiǎn)單的 exclusionPaths 屬性就可以做到這一點(diǎn)。
有些功能未能提供,比如,內(nèi)嵌表格,以及對(duì)非圖像的附件的支持。
盡管有這些區(qū)別,總的來(lái)說(shuō)系統(tǒng)還是一樣的。NSTextStorage 在兩個(gè)系統(tǒng)是是一模一樣的,NSLayoutManager 和 NSTextContainer 也沒(méi)有太大的不同。這些變動(dòng),在沒(méi)有太多去除對(duì)一些特例的支持的情況下,看來(lái)(某些情況下大大地)使文本系統(tǒng)的使用變得更為容易。我認(rèn)為這是件好事。
事后回顧我從 Apple 工程師那里得到的關(guān)于將 Cocoa 文本系統(tǒng)移植到 iOS 的答案,我們可以得到一些背景信息。拖到現(xiàn)在并削減功能的原因很簡(jiǎn)單:性能、性能、性能。文本布局可能是極度昂貴的任務(wù)——內(nèi)存方面、電量方面以及時(shí)間方面——特別是在移動(dòng)設(shè)備上。Apple 必須采用更簡(jiǎn)單的解決方案,并等到處理能力能夠至少部分支持一個(gè)完善的文本布局引擎。
示例
為了說(shuō)明 TextKit 的能力,我創(chuàng)建了一個(gè)小的演示項(xiàng)目,你可以在 GitHub 上找到它。在這個(gè)演示程序中,我只完成了一些以前不容易完成的功能。我必須承認(rèn)編碼工作只花了我禮拜天的一個(gè)上午的時(shí)間;如果以前要做同樣的事情,我得花幾天甚至幾個(gè)星期。
TextKit 包括了超過(guò) 100 個(gè)方法,一篇文章根本沒(méi)辦法盡數(shù)涉及。而事實(shí)上,大多數(shù)時(shí)候,你需要的僅僅是一個(gè)正確的方法,TextKit 的使用和定制性也仍有待探索。所以我決定做四個(gè)更小的演示程序,而非一個(gè)大的演示程序來(lái)展示所有功能。每個(gè)演示程序中,我試著演示針對(duì)不同的方面和不同的類進(jìn)行定制。
演示程序1:配置
讓我們從最簡(jiǎn)單的開(kāi)始:配置文本系統(tǒng)。正如你在上面 TextKit 簡(jiǎn)圖中看到的,NSTextStorage、NSLayoutManager 和 NSTextContainer 之間的箭頭都是有兩個(gè)頭的。我試圖描述它們的關(guān)系是 1 對(duì) N 的關(guān)系。就是那樣:一個(gè)文本存儲(chǔ)可以擁有多個(gè)布局管理器,一個(gè)布局管理器也可以擁有多個(gè)文本容器。這些多重性帶來(lái)了很好的特性:
- 將多個(gè)文本管理器附加到一個(gè)文本存儲(chǔ)上,可以產(chǎn)生相同文本的多種視覺(jué)表現(xiàn),而且它們可以并排顯示。每一個(gè)表現(xiàn)可以獨(dú)立地布置和修改大小。如果相應(yīng)的文本視圖可編輯,那么在某個(gè)視圖上做的所有修改都會(huì)馬上反映到所有視圖上。
- 將多個(gè)文本容器附加到一個(gè)文本管理器上,可以將一個(gè)文本分布到多個(gè)視圖展現(xiàn)出來(lái)。例如很有用的基于頁(yè)面的布局:每個(gè)頁(yè)面包含一個(gè)單獨(dú)的視圖。一個(gè)文本管理器利用這些視圖的文本容器,將文本分布到這些視圖上。
在 storyboard 或者 interface 文件中實(shí)例化 UITextView 時(shí),它會(huì)預(yù)配置一個(gè)文本系統(tǒng):一個(gè)文本存儲(chǔ),引用一個(gè)文本管理器,而后者又引用一個(gè)文本容器。同樣地,一個(gè)文本系統(tǒng)棧也可以通過(guò)代碼直接創(chuàng)建:
| 1 2 3 4 5 6 7 8 9 10 | NSTextStorage *textStorage = [NSTextStorage new]; NSLayoutManager *layoutManager = [NSLayoutManager new]; [textStorage addLayoutManager: layoutManager]; NSTextContainer *textContainer = [NSTextContainer new]; [layoutManager addTextContainer: textContainer]; UITextView *textView = [[UITextView alloc] initWithFrame:someFrame ???????????????????????????????????????????textContainer:textContainer]; |
這是最簡(jiǎn)單的方式。手工創(chuàng)建一個(gè)文本系統(tǒng),唯一需要記住的事情是你的視圖控制器必須 retain 文本存儲(chǔ)。在棧底的文本視圖只保留了對(duì)文本存儲(chǔ)和布局管理器的弱引用。當(dāng)文本存儲(chǔ)被釋放時(shí),布局管理器也被釋放了,這樣留給文本視圖的就只有一個(gè)斷開(kāi)的容器了。
這個(gè)規(guī)則有一個(gè)例外。只有從一個(gè) interface 文件或 storyboard 實(shí)例化一個(gè)文本視圖時(shí),文本視圖確實(shí)會(huì) retain 文本存儲(chǔ)。框架使用了一些黑魔法以確保所有的對(duì)象都被 retain,而無(wú)需建立一個(gè) retain 環(huán)。
記住這些之后,創(chuàng)建一個(gè)更高級(jí)的設(shè)置也非常簡(jiǎn)單。假設(shè)在一個(gè)視圖里面依舊有一個(gè)從 nib 實(shí)例化的文本視圖,叫做 originalTextView。增加對(duì)相同文本的第二個(gè)文本視圖只需要復(fù)制上面的代碼,并重用 originalTextView 的文本存儲(chǔ):
| 1 2 3 4 5 6 7 8 9 10 | NSTextStorage *sharedTextStorage = originalTextView.textStorage; NSLayoutManager *otherLayoutManager = [NSLayoutManager new]; [sharedTextStorage addLayoutManager: otherLayoutManager]; NSTextContainer *otherTextContainer = [NSTextContainer new]; [otherLayoutManager addTextContainer: otherTextContainer]; UITextView *otherTextView = [[UITextView alloc] initWithFrame:someFrame ????????????????????????????????????????????????textContainer:otherTextContainer]; |
將第二個(gè)文本容器附加到布局管理器也差不多。比方說(shuō)我們希望上面例子中的文本填充兩個(gè)文本視圖,而非一個(gè)。簡(jiǎn)單:
| 1 2 3 4 5 | NSTextContainer *thirdTextContainer = [NSTextContainer new]; [otherLayoutManager addTextContainer: thirdTextContainer]; UITextView *thirdTextView = [[UITextView alloc] initWithFrame:someFrame ????????????????????????????????????????????????textContainer:thirdTextContainer]; |
但有一點(diǎn)需要注意:由于在 otherTextView 中的文本容器可以無(wú)限地調(diào)整大小,thirdTextView 永遠(yuǎn)不會(huì)得到任何文本。因此,我們必須指定文本應(yīng)該從一個(gè)視圖回流到其它視圖,而不應(yīng)該調(diào)整大小或者滾動(dòng):
| 1 | otherTextView.scrollEnabled = NO; |
不幸的是,看來(lái)將多個(gè)文本容器附加到一個(gè)文本管理器會(huì)禁用編輯功能。如果必須保留編輯功能的話,你只可以將一個(gè)文本容器附加到一個(gè)文本管理器上。
想要一個(gè)這個(gè)配置的可運(yùn)行的例子的話,請(qǐng)?jiān)谇懊嫣岬降? TextKitDemo 中查看“Configuration”標(biāo)簽頁(yè)。
演示程序2:語(yǔ)法高亮
如果配置文本視圖不是那么令人激動(dòng),那么這里有更有趣的:語(yǔ)法高亮!
看看 TextKit 組件的責(zé)任劃分,就很清楚語(yǔ)法高亮應(yīng)該在文本存儲(chǔ)上實(shí)現(xiàn)。因?yàn)?NSTextStorage 是一個(gè)類簇(附注3),創(chuàng)建它的子類需要做不少工作。我的想法是建立一個(gè)復(fù)合對(duì)象:實(shí)現(xiàn)所有的方法,但只是將對(duì)它們的調(diào)用轉(zhuǎn)發(fā)給一個(gè)實(shí)際的實(shí)例,將輸入輸出參數(shù)或者結(jié)果修改為希望的樣子。
NSTextStorage 繼承自 NSMutableAttributedString,并且必須實(shí)現(xiàn)以下四個(gè)方法——兩個(gè) getter 和兩個(gè) setter:
| 1 2 3 4 5 | - (NSString *)string; - (NSDictionary *)attributesAtIndex:(NSUInteger)location ?????????????????????effectiveRange:(NSRangePointer)range; - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str; - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range; |
一個(gè)類簇的子類的復(fù)合對(duì)象的實(shí)現(xiàn)也相當(dāng)簡(jiǎn)單。首先,找到一個(gè)滿足所有要求的最簡(jiǎn)單的類。在我們的例子中,它是 NSMutableAttributedString,我們用它作為實(shí)現(xiàn)自定義存儲(chǔ)的實(shí)現(xiàn):
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | @implementation TKDHighlightingTextStorage { ????NSMutableAttributedString *_imp; } - (id)init { ????self = [super init]; ????if (self) { ????????_imp = [NSMutableAttributedString new]; ????} ????return self; } |
有了這個(gè)對(duì)象,只需要一行代碼就可以實(shí)現(xiàn)兩個(gè) getter 方法:
| 1 2 3 4 5 6 7 8 9 | - (NSString *)string { ????return _imp.string; } - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range { ????return [_imp attributesAtIndex:location effectiveRange:range]; } |
實(shí)現(xiàn)兩個(gè) setter 方法也幾乎同樣簡(jiǎn)單。但也有一個(gè)小麻煩:文本存儲(chǔ)需要通知它的文本管理器變化發(fā)生了。因此 settter 方法必須也要調(diào)用 -edited:range:changeInLegth: 并傳給它變化的描述。聽(tīng)起來(lái)更糟糕,實(shí)現(xiàn)變成:
| 1 2 3 4 5 6 7 8 9 10 11 12 | - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str { ????[_imp replaceCharactersInRange:range withString:str]; ????[self edited:NSTextStorageEditedCharacters range:range ??????????????????????????????????????changeInLength:(NSInteger)str.length - (NSInteger)range.length]; } - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range { ????[_imp setAttributes:attrs range:range]; ????[self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; } |
就這樣,我們?cè)谖谋鞠到y(tǒng)棧里面有了一個(gè)文本存儲(chǔ)的全功能替換版本。在從 interface 文件中載入時(shí),可以像這樣將它插入文本視圖——但是記住從一個(gè)實(shí)例變量引用文本存儲(chǔ):
| 1 2 | _textStorage = [TKDHighlightingTextStorage new]; [_textStorage addLayoutManager: self.textView.layoutManager]; |
到目前為止,一切都很好。我們?cè)O(shè)法插入了一個(gè)自定義的文本存儲(chǔ),接下來(lái)我們需要真正高亮文本的某些部分了。現(xiàn)在,一個(gè)簡(jiǎn)單的高亮應(yīng)該就是夠了:我們希望將所有 iWords 的顏色變成紅色——也就是那些以小寫“i”開(kāi)頭,后面跟著一個(gè)大寫字母的單詞。
一個(gè)方便實(shí)現(xiàn)高亮的辦法是覆蓋 -processEditing。每次文本存儲(chǔ)有修改時(shí),這個(gè)方法都自動(dòng)被調(diào)用。每次編輯后,NSTextStorage 會(huì)用這個(gè)方法來(lái)清理字符串。例如,有些字符無(wú)法用選定的字體顯示時(shí),文本存儲(chǔ)使用一個(gè)可以顯示它們的字體來(lái)進(jìn)行替換。
和其它一樣,為 iWords 增加一個(gè)簡(jiǎn)單的高亮也相當(dāng)簡(jiǎn)單。我們覆蓋 -processEditing,調(diào)用父類的實(shí)現(xiàn),并設(shè)置一個(gè)正則表達(dá)式來(lái)查找單詞:
| 1 2 3 4 5 6 7 8 9 | - (void)processEditing { ????[super processEditing]; ????static NSRegularExpression *iExpression; ????NSString *pattern = @"i[\\p{Alphabetic}&&\\p{Uppercase}][\\p{Alphabetic}]+"; ????iExpression = iExpression ?: [NSRegularExpression regularExpressionWithPattern:pattern ???????????????????????????????????????????????????????????????????????????options:0 ?????????????????????????????????????????????????????????????????????????????error:NULL]; |
然后,首先清除之前的所有高亮:
| 1 2 | NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange]; [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange]; |
其次遍歷所有的樣式匹配項(xiàng)并高亮它們:
| 1 2 3 4 5 6 7 | ????[iExpression enumerateMatchesInString:self.string ??????????????????????????????????options:0 range:paragaphRange ???????????????????????????????usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) ????{ ????????[self addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:result.range]; ????}]; } |
就是這樣。我們創(chuàng)建了一個(gè)支持語(yǔ)法高亮的動(dòng)態(tài)文本視圖。當(dāng)用戶鍵入時(shí),高亮將被實(shí)時(shí)應(yīng)用。而且這只需幾行代碼。酷吧?
請(qǐng)注意僅僅使用 edited range 是不夠的。例如,當(dāng)手動(dòng)鍵入 iWords,只有一個(gè)單詞的第三個(gè)字符被鍵入后,正則表達(dá)式才開(kāi)始匹配。但那時(shí) editedRange 僅包含第三個(gè)字符,因此所有的處理只會(huì)檢查這個(gè)字符。通過(guò)重新處理整個(gè)段落,我們可以完成高亮功能,又不會(huì)太過(guò)影響性能。
想要一個(gè)這個(gè)配置的可運(yùn)行的例子的話,請(qǐng)?jiān)谇懊嫣岬降? TextKitDemo 中查看“Highlighting”標(biāo)簽頁(yè)。
演示程序3:布局修改
如前所述,布局管理器是核心的布局主力。Mac OS 上 NSTypesetter 的高度可定制功能被并入 iOS 上的 NSLayoutManager。雖然 TextKit 不具備像 Cocoa 文本系統(tǒng)那樣的完全可定制性,但它提供很多代理方法來(lái)允許做一些調(diào)整。如前所述,TextKit 與 CoreText 更緊密地集成在一起,主要是基于性能方面的考慮。但是兩個(gè)文本系統(tǒng)的理念在一定程度上是不一樣的:
Cocoa 文本系統(tǒng):在 Mac OS上,性能不是問(wèn)題,設(shè)計(jì)考量的全部是靈活性。可能是這樣:“這個(gè)東西可以做這個(gè)事情。如果你想的話,你可以覆蓋它。性能不是問(wèn)題。你也可以提供完全由自己實(shí)現(xiàn)的字符到字形的轉(zhuǎn)換,去做吧…”
TextKit:性能看來(lái)真是個(gè)問(wèn)題。理念(起碼現(xiàn)在)更多的是像這樣:“我們用簡(jiǎn)單但是高性能的方法實(shí)現(xiàn)了這個(gè)功能。這是結(jié)果,但是我們給你一個(gè)機(jī)會(huì)去更改它的一些東西。但是你只能在不太損害性能的地方進(jìn)行修改。”
足夠的理念,讓我們來(lái)定制些東西。例如,調(diào)整行高如何?聽(tīng)起來(lái)不可思議,但是在之前的 iOS 發(fā)布版上調(diào)整行高至少是很黑客的行為,或者需要使用私有 API。幸運(yùn)的是,現(xiàn)在(再一次)不用那么搞腦子了。設(shè)置布局管理器的代理并實(shí)現(xiàn)僅僅一個(gè)方法即可:
| 1 2 3 4 5 6 | - (CGFloat)????? layoutManager:(NSLayoutManager *)layoutManager ??lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex ??withProposedLineFragmentRect:(CGRect)rect { ????return floorf(glyphIndex / 100); } |
在以上的代碼中,我修改了行間距,讓它與文本長(zhǎng)度同時(shí)增長(zhǎng)。這導(dǎo)致頂部的行比底部的行排列得更緊密。我承認(rèn)這沒(méi)什么實(shí)際的用處,但是它是可以做到的(而且肯定會(huì)有更實(shí)用的用例的)。
好,來(lái)一個(gè)更現(xiàn)實(shí)的場(chǎng)景。假設(shè)你的文本中有鏈接,你不希望這些鏈接被行包圍。如果可能的話,一個(gè) URL 應(yīng)該始終顯示為一個(gè)整體,一個(gè)單一的文本片段。沒(méi)有什么比這更簡(jiǎn)單的了。
首先,我們通過(guò)使用自定義的文本存儲(chǔ),就像前面討論過(guò)的那個(gè)。但是,它尋找鏈接并將其標(biāo)記,而不是檢測(cè) iWords,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | static NSDataDetector *linkDetector; linkDetector = linkDetector ?: [[NSDataDetector alloc] initWithTypes:NSTextCheckingTypeLink error:NULL]; NSRange paragaphRange = [self.string paragraphRangeForRange: NSMakeRange(range.location, str.length)]; [self removeAttribute:NSLinkAttributeName range:paragaphRange]; [linkDetector enumerateMatchesInString:self.string ???????????????????????????????options:0 ?????????????????????????????????range:paragaphRange ????????????????????????????usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { ????[self addAttribute:NSLinkAttributeName value:result.URL range:result.range]; }]; |
有了這個(gè),改變斷行行為就只需要實(shí)現(xiàn)一個(gè)布局管理器的代理方法:
| 1 2 3 4 5 6 7 8 9 | - (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex { ????NSRange range; ????NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName ??????????????????????????????????????????????????atIndex:charIndex ???????????????????????????????????????????effectiveRange:&range]; ????return !(linkURL && charIndex > range.location && charIndex <= NSMaxRange(range)); } |
想要一個(gè)可運(yùn)行的例子的話,請(qǐng)?jiān)谇懊嫣岬降? TextKitDemo 中查看“Layout”標(biāo)簽頁(yè)。以下是截屏:
順便說(shuō)一句,上面截屏里面的綠色輪廓線是無(wú)法用 TextKit 實(shí)現(xiàn)的。在這個(gè)演示程序中,我用了個(gè)小技巧來(lái)在布局管理器的子類中給文本畫輪廓線。也可以很容易以特定的方法來(lái)擴(kuò)展 TextKit 的繪制功能。一定要看看!
演示程序4:文本交互
前面已經(jīng)涉及到了 NSTextStorage 和 NSLayoutManager,最后一個(gè)演示程序?qū)⑸婕?NSTextContainer。這個(gè)類并不復(fù)雜,而且它除了指定文本可不可以放置在某個(gè)地方外,什么都沒(méi)做。
不要將文本放置在某些區(qū)域,這是很常見(jiàn)的需求,例如,在雜志應(yīng)用中。對(duì)于這種情況,iOS 上的 NSTextContainer 提供了一個(gè) Mac 開(kāi)發(fā)者夢(mèng)寐以求的屬性:exclusionPaths,它允許開(kāi)發(fā)者設(shè)置一個(gè) NSBezierPath 數(shù)組來(lái)指定不可填充文本的區(qū)域。要了解這到底是什么東西,看一眼下面的截屏:
正如你所看到的,所有的文本都放置在藍(lán)色橢圓外面。在文本視圖里面實(shí)現(xiàn)這個(gè)行為很簡(jiǎn)單,但是有個(gè)小麻煩:貝塞爾路徑的坐標(biāo)必須使用容器的坐標(biāo)系。以下是轉(zhuǎn)換方法:
| 1 2 3 4 5 6 7 8 9 10 11 | - (void)updateExclusionPaths { ????CGRect ovalFrame = [self.textView convertRect:self.circleView.bounds ?????????????????????????????????????????fromView:self.circleView]; ????ovalFrame.origin.x -= self.textView.textContainerInset.left; ????ovalFrame.origin.y -= self.textView.textContainerInset.top; ????UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect:ovalFrame]; ????self.textView.textContainer.exclusionPaths = @[ovalPath]; } |
在這個(gè)例子中,我使用了一個(gè)用戶可移動(dòng)的視圖,它可以被自由移動(dòng),而文本會(huì)實(shí)時(shí)地圍繞著它重新排版。我們首先將它的bounds(self.circleView.bounds)轉(zhuǎn)換到文本視圖的坐標(biāo)系統(tǒng)。
因?yàn)闆](méi)有 inset,文本會(huì)過(guò)于靠近視圖邊界,所以 UITextView 會(huì)在離邊界還有幾個(gè)點(diǎn)的距離的地方插入它的文本容器。因此,要得到以容器坐標(biāo)表示的路徑,必須從 origin 中減去這個(gè)插入點(diǎn)的坐標(biāo)。
在此之后,只需將貝塞爾路徑設(shè)置給文本容器即可將對(duì)應(yīng)的區(qū)域排除掉。其它的過(guò)程對(duì)你來(lái)說(shuō)是透明的,TextKit 會(huì)自動(dòng)處理。
想要一個(gè)可運(yùn)行的例子的話,請(qǐng)?jiān)谇懊嫣岬降? TextKitDemo 中查看“Interaction”標(biāo)簽頁(yè)。作為一個(gè)小噱頭,它也包含了一個(gè)跟隨當(dāng)前文本選擇的視圖。應(yīng)為,你也知道,沒(méi)有一個(gè)小小的丑陋的煩人的回形針擋住你的話,那還是一個(gè)好的文本編輯器演示程序嗎?
1. Pages 確實(shí)——據(jù) Apple 聲稱——絕對(duì)沒(méi)有使用私有 API。*咳* 我的理論:它要么使用了一個(gè) TextKit 的史前版本,要么復(fù)制了 UIKit 一半的私有源程序。或者兩者的混合。
2. 字形:如果說(shuō)字符是一個(gè)字母的“語(yǔ)義”表達(dá),字形則是它的可視化表達(dá)。取決于所使用的字體,字形要么是貝塞爾路徑,或者位圖圖像,它定義了要繪制出來(lái)的形狀。也請(qǐng)參考卓越的 Wikipedia 上關(guān)于字形的這篇文章。
3. 在一個(gè)類簇中,只有一個(gè)抽象的父類是公共的。分配一個(gè)實(shí)例實(shí)際上就是創(chuàng)建其中一個(gè)私有類的對(duì)象。因此,你總是為一個(gè)抽象類創(chuàng)建子類,并且需要實(shí)現(xiàn)所有的方法。也請(qǐng)參考 class cluster documentation。
總結(jié)
以上是生活随笔為你收集整理的iOS上文本处理之简史的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 优酷限免胡歌所有剧!《仙剑》等经典剧限时
- 下一篇: Adopting Modern Obje