Skip to content

Commit 8707b83

Browse files
committed
修改行高计算逻辑
1 parent 754d5ca commit 8707b83

4 files changed

Lines changed: 82 additions & 186 deletions

File tree

AttributedString.xcodeproj/project.pbxproj

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@
8787
9BE7D4C92488E0FC00DE1176 /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE7D4C52488E0FC00DE1176 /* Action.swift */; };
8888
9BEBB11324D511A000355C22 /* UILabelLayoutManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBB11224D511A000355C22 /* UILabelLayoutManagerDelegate.swift */; };
8989
9BEBB11424D511A000355C22 /* UILabelLayoutManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBB11224D511A000355C22 /* UILabelLayoutManagerDelegate.swift */; };
90-
9BEBB11624D5135D00355C22 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBB11524D5135D00355C22 /* UIFontExtension.swift */; };
91-
9BEBB11724D5135D00355C22 /* UIFontExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBB11524D5135D00355C22 /* UIFontExtension.swift */; };
9290
/* End PBXBuildFile section */
9391

9492
/* Begin PBXContainerItemProxy section */
@@ -152,7 +150,6 @@
152150
9BBC525E24A053A600CBDFFF /* Checking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checking.swift; sourceTree = "<group>"; };
153151
9BE7D4C52488E0FC00DE1176 /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = "<group>"; };
154152
9BEBB11224D511A000355C22 /* UILabelLayoutManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabelLayoutManagerDelegate.swift; sourceTree = "<group>"; };
155-
9BEBB11524D5135D00355C22 /* UIFontExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFontExtension.swift; sourceTree = "<group>"; };
156153
/* End PBXFileReference section */
157154

158155
/* Begin PBXFrameworksBuildPhase section */
@@ -321,7 +318,6 @@
321318
children = (
322319
9B6E89AB238274CB009EBEBE /* UILabelExtension.swift */,
323320
9BEBB11224D511A000355C22 /* UILabelLayoutManagerDelegate.swift */,
324-
9BEBB11524D5135D00355C22 /* UIFontExtension.swift */,
325321
);
326322
path = UILabel;
327323
sourceTree = "<group>";
@@ -652,7 +648,6 @@
652648
9BEBB11424D511A000355C22 /* UILabelLayoutManagerDelegate.swift in Sources */,
653649
9B267BE724408357002F571E /* ShadowExtension.swift in Sources */,
654650
9B267BE824408357002F571E /* UIButtonExtension.swift in Sources */,
655-
9BEBB11724D5135D00355C22 /* UIFontExtension.swift in Sources */,
656651
9B8765A324850742009C51C2 /* ObjectExtension.swift in Sources */,
657652
9B267BB32440811F002F571E /* CGSizeExtension.swift in Sources */,
658653
9B267BB42440811F002F571E /* CGRectExtension.swift in Sources */,
@@ -709,7 +704,6 @@
709704
9BEBB11324D511A000355C22 /* UILabelLayoutManagerDelegate.swift in Sources */,
710705
9B6E89A223826D8E009EBEBE /* Interpolation.swift in Sources */,
711706
9B6E89B3238276B0009EBEBE /* CGPointExtension.swift in Sources */,
712-
9BEBB11624D5135D00355C22 /* UIFontExtension.swift in Sources */,
713707
9B8765A024850742009C51C2 /* ObjectExtension.swift in Sources */,
714708
9B6E89DF23828F7C009EBEBE /* ShadowExtension.swift in Sources */,
715709
9B6E89AF238275D2009EBEBE /* CGSizeExtension.swift in Sources */,

Sources/Extension/UIKit/UILabel/UIFontExtension.swift

Lines changed: 0 additions & 80 deletions
This file was deleted.

Sources/Extension/UIKit/UILabel/UILabelExtension.swift

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ extension AttributedStringWrapper where Base: UILabel {
3131
public var text: AttributedString? {
3232
get { base.touched?.0 ?? AttributedString(base.attributedText) }
3333
set {
34-
// 字体补丁 交互所有字体的属性 更换为系统字体的值
35-
UIFont.Patch
3634
// 判断当前是否在触摸状态, 内容是否发生了变化
3735
if var touched = base.touched, touched.0.isContentEqual(to: newValue) {
3836
guard let string = newValue else {
@@ -334,22 +332,14 @@ fileprivate extension UILabel {
334332
let text = adaptation(scaledAttributedText ?? synthesizedAttributedText ?? attributedText)
335333
guard let attributedString = AttributedString(text) else { return nil }
336334

337-
struct BaseLineInfo {
338-
let firstBaseline: Double
339-
let lastBaseline: Double
340-
let referenceBounds: CGRect
341-
let measuredNumberOfLines: Int64
342-
}
343-
344335
// 构建同步Label设置的TextKit
345336
let textStorage = NSTextStorage(attributedString: attributedString.value)
346-
let textContainer = NSTextContainer(size: .init(bounds.size.width, bounds.size.height + 1))
337+
let textContainer = NSTextContainer(size: .init(bounds.size.width, bounds.size.height))
347338
let layoutManager = NSLayoutManager()
348339
layoutManager.delegate = UILabelLayoutManagerDelegate.shared // 重新计算行高确保TextKit与UILabel显示一致
349340
textContainer.lineBreakMode = lineBreakMode
350341
textContainer.lineFragmentPadding = 0.0
351342
textContainer.maximumNumberOfLines = numberOfLines
352-
layoutManager.usesFontLeading = false
353343
layoutManager.addTextContainer(textContainer)
354344
textStorage.addLayoutManager(layoutManager)
355345

Sources/Extension/UIKit/UILabel/UILabelLayoutManagerDelegate.swift

Lines changed: 81 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -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

9583
extension 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

Comments
 (0)