@@ -25,138 +25,130 @@ class UILabelLayoutManagerDelegate: NSObject, NSLayoutManagerDelegate {
2525 baselineOffset: UnsafeMutablePointer < CGFloat > ,
2626 in textContainer: NSTextContainer ,
2727 forGlyphRange glyphRange: NSRange ) -> Bool {
28-
29- guard let textStorage = layoutManager. textStorage else {
30- return false
31- }
32- // 获取当前所有属性
33- let attributes = getAttributes ( layoutManager, with: textStorage, for: glyphRange)
34- // 如果有附件 直接跳过 可以解决附件导致的计算错误
35- guard !attributes. contains ( where: { $0. attributes [ . attachment] != nil } ) else {
36- return false
37- }
38- // 获取行高最大的属性
39- guard
40- let item = getMaxAttributes ( attributes) ,
41- let font = item. attributes [ . font] as? UIFont ,
42- let paragraph = item. attributes [ . paragraphStyle] as? NSParagraphStyle else {
43- return false
44- }
45- var rect = lineFragmentRect. pointee
46- var used = lineFragmentUsedRect. pointee
47-
48- let defaultFont = UIFont . systemFont ( ofSize: font. pointSize)
49- let lineHeight = getLineHeight ( defaultFont, with: paragraph)
50- var baseline = lineHeight + defaultFont. descender
51- rect. size. height = lineHeight
52-
53- used. size. height = lineHeight
5428 /**
5529 From apple's doc:
5630 https://developer.apple.com/library/content/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/CustomTextProcessing/CustomTextProcessing.html
5731 In addition to returning the line fragment rectangle itself, the layout manager returns a rectangle called the used rectangle. This is the portion of the line fragment rectangle that actually contains glyphs or other marks to be drawn. By convention, both rectangles include the line fragment padding and the interline space (which is calculated from the font’s line height metrics and the paragraph’s line spacing parameters). However, the paragraph spacing (before and after) and any space added around the text, such as that caused by center-spaced text, are included only in the line fragment rectangle, and are not included in the used rectangle.
5832 */
59-
60- // 行间距
61- rect. size. height += paragraph. lineSpacing
33+ guard let textStorage = layoutManager. textStorage else {
34+ return false
35+ }
36+ guard let maximum = getMaximum ( layoutManager, with: textStorage, for: glyphRange) else {
37+ return false
38+ }
6239
63- // 段落间距
64- if paragraph. paragraphSpacing > 0 {
65- let lastIndex = layoutManager. characterIndexForGlyph ( at: glyphRange. location + glyphRange. length - 1 )
40+ // 段落前间距
41+ var paragraphSpacingBefore : CGFloat = 0
42+ if glyphRange. location > 0 , let paragraph = maximum. paragraph, paragraph. paragraphSpacingBefore > . ulpOfOne {
43+ let lastIndex = layoutManager. characterIndexForGlyph ( at: glyphRange. location - 1 )
6644 let substring = textStorage. attributedSubstring ( from: . init( location: lastIndex, length: 1 ) ) . string
6745 let isLineBreak = substring == " \n "
68- rect . size . height + = isLineBreak ? paragraph. paragraphSpacing : 0
46+ paragraphSpacingBefore = isLineBreak ? paragraph. paragraphSpacingBefore : 0
6947 }
7048
71- // 段落前间距
72- if glyphRange. location > 0 , paragraph. paragraphSpacingBefore > 0 {
73- let lastIndex = layoutManager. characterIndexForGlyph ( at: glyphRange. location - 1 )
49+ // 段落间距
50+ var paragraphSpacing : CGFloat = 0
51+ if let paragraph = maximum. paragraph, paragraph. paragraphSpacing > . ulpOfOne {
52+ let lastIndex = layoutManager. characterIndexForGlyph ( at: glyphRange. location + glyphRange. length - 1 )
7453 let substring = textStorage. attributedSubstring ( from: . init( location: lastIndex, length: 1 ) ) . string
7554 let isLineBreak = substring == " \n "
76- let space = isLineBreak ? paragraph. paragraphSpacingBefore : 0
77- rect. size. height += space
78- used. origin. y += space
79- baseline += space
55+ paragraphSpacing = isLineBreak ? paragraph. paragraphSpacing : 0
8056 }
8157
58+ var rect = lineFragmentRect. pointee
59+ var used = lineFragmentUsedRect. pointee
60+ used. size. height = max ( maximum. lineHeight, used. height)
61+ rect. size. height = used. height + maximum. lineSpacing + paragraphSpacing + paragraphSpacingBefore
62+
8263 // 重新赋值最终结果
8364 lineFragmentRect. pointee = rect
8465 lineFragmentUsedRect. pointee = used
85- baselineOffset. pointee = baseline
8666
87- return true
67+ /**
68+ From apple's doc:
69+ true if you modified the layout information and want your modifications to be used or false if the original layout information should be used.
70+ But actually returning false is also used. : )
71+ We should do this to solve the problem of exclusionPaths not working.
72+ */
73+ return false
8874 }
8975
76+ // Implementing this method with a return value 0 will solve the problem of last line disappearing
77+ // when both maxNumberOfLines and lineSpacing are set, since we didn't include the lineSpacing in the lineFragmentUsedRect.
9078 func layoutManager( _ layoutManager: NSLayoutManager , lineSpacingAfterGlyphAt glyphIndex: Int , withProposedLineFragmentRect rect: CGRect ) -> CGFloat {
9179 return 0
9280 }
9381}
9482
9583extension UILabelLayoutManagerDelegate {
9684
85+ private struct Maximum {
86+ let font : UIFont
87+ let lineHeight : CGFloat
88+ let lineSpacing : CGFloat
89+ let paragraph : NSParagraphStyle ?
90+ }
91+
92+ private func getMaximum( _ layoutManager: NSLayoutManager , with textStorage: NSTextStorage , for glyphRange: NSRange ) -> Maximum ? {
93+ let characterRange = layoutManager. characterRange ( forGlyphRange: glyphRange, actualGlyphRange: nil )
94+
95+ var maximumLineHeightFont : UIFont ?
96+ var maximumLineHeight : CGFloat = 0
97+ var maximumLineSpacing : CGFloat = 0
98+ var paragraph : NSParagraphStyle ?
99+ textStorage. enumerateAttributes ( in: characterRange, options: . longestEffectiveRangeNotRequired) {
100+ ( attributes, range, stop) in
101+ // 实际计算使用的是 NSOriginalFont lineHeight.
102+ print ( attributes [ . originalFont] )
103+ guard let font = ( attributes [ . originalFont] ?? attributes [ . font] ) as? UIFont else { return }
104+ paragraph = paragraph ?? attributes [ . paragraphStyle] as? NSParagraphStyle
105+
106+ let lineHeight = getLineHeight ( font, with: paragraph)
107+ // 获取最大行高
108+ if lineHeight > maximumLineHeight {
109+ maximumLineHeightFont = font
110+ maximumLineHeight = lineHeight
111+ }
112+ // 获取最大行间距
113+ if let lineSpacing = paragraph? . lineSpacing, lineSpacing > maximumLineSpacing {
114+ maximumLineSpacing = lineSpacing
115+ }
116+ }
117+
118+ guard let font = maximumLineHeightFont else {
119+ return nil
120+ }
121+ return . init(
122+ font: font,
123+ lineHeight: maximumLineHeight,
124+ lineSpacing: maximumLineSpacing,
125+ paragraph: paragraph
126+ )
127+ }
128+
97129 private func getLineHeight( _ font: UIFont , with paragraph: NSParagraphStyle ? = . none) -> CGFloat {
98130 guard let paragraph = paragraph else {
99131 return font. lineHeight
100132 }
101133
102134 var lineHeight = font. lineHeight
103135
104- if paragraph. lineHeightMultiple > 0 {
136+ if paragraph. lineHeightMultiple > . ulpOfOne {
105137 lineHeight *= paragraph. lineHeightMultiple
106138 }
107- if paragraph. minimumLineHeight > 0 {
139+ if paragraph. minimumLineHeight > . ulpOfOne {
108140 lineHeight = max ( paragraph. minimumLineHeight, lineHeight)
109141 }
110- if paragraph. maximumLineHeight > 0 {
142+ if paragraph. maximumLineHeight > . ulpOfOne {
111143 lineHeight = min ( paragraph. maximumLineHeight, lineHeight)
112144 }
113145 return lineHeight
114146 }
147+ }
148+
149+ extension NSAttributedString . Key {
115150
116- private typealias Item = ( range: NSRange , attributes: [ NSAttributedString . Key : Any ] )
117-
118- private func getMaxAttributes( _ attributes: [ Item ] ) -> Item ? {
119- return attributes. max { ( l, r) -> Bool in
120- guard
121- let lf = l. attributes [ . font] as? UIFont ,
122- let rf = r. attributes [ . font] as? UIFont else {
123- return false
124- }
125-
126- let lp = l. attributes [ . paragraphStyle] as? NSParagraphStyle
127- let rp = r. attributes [ . paragraphStyle] as? NSParagraphStyle
128- return getLineHeight ( lf, with: lp) < getLineHeight ( rf, with: rp)
129- }
130- }
131-
132- private func getAttributes( _ layoutManager: NSLayoutManager , with textStorage: NSTextStorage , for glyphRange: NSRange ) -> [ Item ] {
133- var glyphRange = glyphRange
134-
135- // 排除换行符。系统不能用它计算直线。
136- if glyphRange. length > 1 {
137- let lastIndex = glyphRange. location + glyphRange. length - 1
138- if layoutManager. propertyForGlyph ( at: lastIndex) == . controlCharacter {
139- glyphRange = NSRange ( location: glyphRange. location, length: glyphRange. length - 1 )
140- }
141- }
142- // 循环遍历获取当前字形范围内的所有属性
143- let targetRange = layoutManager. characterRange ( forGlyphRange: glyphRange, actualGlyphRange: nil )
144- var array : [ ( NSRange , [ NSAttributedString . Key : Any ] ) ] = [ ]
145- var lastIndex = - 1
146- var effectiveRange = NSRange ( location: targetRange. location, length: 0 )
147- while ( effectiveRange. location + effectiveRange. length < targetRange. location + targetRange. length) {
148- var current = effectiveRange. location + effectiveRange. length
149- if current <= lastIndex {
150- current += 1
151- }
152- let attributes = textStorage. attributes ( at: current, effectiveRange: & effectiveRange)
153- if !attributes. isEmpty {
154- array. append ( ( effectiveRange, attributes) )
155- }
156- lastIndex = current
157- }
158- return array
159- }
151+ static let originalFont : NSAttributedString . Key = . init( " NSOriginalFont " )
160152}
161153
162154#endif
0 commit comments