最近被提了个需求,产品经理觉得iOS自带的 NSLineBreakByWordWrapping 不够好看,对一些英文单词的整体换行引起的留白不满意,核心诉求就一点:给我抄微信!
这是微信的截图,对比上下两条消息,明明下面一条的英文更长,但是第一条消息的英文做了整体换行(word wrapping),第二条消息的英文紧跟着汉字后面,在英文中间做了换行(char wrapping)。一个段落里怎么会有两种换行规则,换行的依据是什么?怎么做到的?
先来明确一下换行的规则是什么?直观看上去的结论是:一般情况下还是按照 word wrapping 来进行换行,在换行处的单词的长度不超过一行时还是进行了整体换行,即第一条消息的情况;第二条消息为特殊情况,当整体换行的英文在换行后仍需要再换行一次时,就执行char wrapping换行。
怎么实现?我个人的解决方案核心在于Text Kit里的NSLayoutManager。关于Text Kit和NSLayoutManager,网上一搜一大片,这里就不赘述了。NSLayoutManager有一个代理方法:
- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex;
看下方法名可以猜出来它的功能:在默认需要换行的位置(记为x)来询问是否执行这次换行,如果return NO,那么就从当前位置往前找一个最近的位置(t < x)来询问是否执行换行,直到return YES为止。如果小于x的所有询问得到的结果都是NO,那么就会打破这次word wrapping,变成了char wrapping。
话不多说上代码。
对代码做一点讲解:
englishWordRanges是一个放英文单词range的数组,遍历这段问题找出连续英文字母串的range,如果不包含英文单词,就用系统的换行规则。
questionIndexArray用来记录要被打破换行规则的位置的数组,questionIndex是当前需要打破的换行位置。matchedIndexArray是所有被标记为换行的位置的数组,lastMatchedIndex是最近一个成功换行的位置。
143行~175行的if逻辑为:因为整个方法会多次调用,为了保证结果的唯一性,则每个成功换行的位置要记录下来,这些位置不能被打破(169行);而之前已经打破规则的位置(即matchedIndex < charIndex < questionIndex),需要继续打破。
181行attributedSubstringFromRange:是整个方法中唯一一处相对耗时的方法,但由于走到这里的次数有限,实测下来对整体性能的影响很小;不过如何更高效的获取lineString.size.width仍然是个优化点。