? ? ? ?最近被提了個(gè)需求,產(chǎn)品經(jīng)理覺得iOS自帶的 NSLineBreakByWordWrapping 不夠好看渐白,對一些英文單詞的整體換行引起的留白不滿意尊浓,核心訴求就一點(diǎn):給我抄微信!
? ? ? ?這是微信的截圖纯衍,對比上下兩條消息栋齿,明明下面一條的英文更長,但是第一條消息的英文做了整體換行(word wrapping),第二條消息的英文緊跟著漢字后面瓦堵,在英文中間做了換行(char wrapping)基协。一個(gè)段落里怎么會有兩種換行規(guī)則,換行的依據(jù)是什么谷丸?怎么做到的堡掏?
? ? ? ?先來明確一下?lián)Q行的規(guī)則是什么?直觀看上去的結(jié)論是:一般情況下還是按照 word wrapping 來進(jìn)行換行刨疼,在換行處的單詞的長度不超過一行時(shí)還是進(jìn)行了整體換行泉唁,即第一條消息的情況;第二條消息為特殊情況揩慕,當(dāng)整體換行的英文在換行后仍需要再換行一次時(shí)亭畜,就執(zhí)行char?wrapping換行。
? ? ? ?怎么實(shí)現(xiàn)迎卤?我個(gè)人的解決方案核心在于Text Kit里的NSLayoutManager拴鸵。關(guān)于Text Kit和NSLayoutManager,網(wǎng)上一搜一大片蜗搔,這里就不贅述了劲藐。NSLayoutManager有一個(gè)代理方法:
- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex;
看下方法名可以猜出來它的功能:在默認(rèn)需要換行的位置(記為x)來詢問是否執(zhí)行這次換行,如果return NO樟凄,那么就從當(dāng)前位置往前找一個(gè)最近的位置(t < x)來詢問是否執(zhí)行換行聘芜,直到return YES為止。如果小于x的所有詢問得到的結(jié)果都是NO缝龄,那么就會打破這次word wrapping汰现,變成了char?wrapping。
? ? ? ?話不多說上代碼叔壤。
? ? ? ?對代碼做一點(diǎn)講解:
? ? ? ?englishWordRanges是一個(gè)放英文單詞range的數(shù)組瞎饲,遍歷這段問題找出連續(xù)英文字母串的range,如果不包含英文單詞炼绘,就用系統(tǒng)的換行規(guī)則嗅战。
? ? ? ?questionIndexArray用來記錄要被打破換行規(guī)則的位置的數(shù)組,questionIndex是當(dāng)前需要打破的換行位置俺亮。matchedIndexArray是所有被標(biāo)記為換行的位置的數(shù)組仗哨,lastMatchedIndex是最近一個(gè)成功換行的位置。
? ? ? ?143行~175行的if邏輯為:因?yàn)檎麄€(gè)方法會多次調(diào)用铅辞,為了保證結(jié)果的唯一性厌漂,則每個(gè)成功換行的位置要記錄下來,這些位置不能被打破(169行)斟珊;而之前已經(jīng)打破規(guī)則的位置(即matchedIndex <?charIndex < questionIndex)苇倡,需要繼續(xù)打破富纸。
? ? ? ?181行attributedSubstringFromRange:是整個(gè)方法中唯一一處相對耗時(shí)的方法,但由于走到這里的次數(shù)有限旨椒,實(shí)測下來對整體性能的影響很邢省;不過如何更高效的獲取lineString.size.width仍然是個(gè)優(yōu)化點(diǎn)综慎。