Skip to content

Commit 90b29b9

Browse files
committed
解决UILabel缩小时 单行对齐问题, 以及多行截断时的对齐问题, 目前还剩一个 \n\n\n的问题没有解决.
1 parent 7b680a2 commit 90b29b9

2 files changed

Lines changed: 98 additions & 97 deletions

File tree

Sources/Extension/UIKit/UILabel/UILabelExtension.swift

Lines changed: 88 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,91 @@ extension UILabel {
254254
}
255255
}
256256

257+
fileprivate extension UILabel {
258+
259+
@objc
260+
func attributedAction(_ sender: UIGestureRecognizer) {
261+
guard isActionEnabled else { return }
262+
guard let action = touched?.2 else { return }
263+
guard action.trigger.matching(sender) else { return }
264+
// 点击 回调
265+
action.handle?()
266+
}
267+
268+
func matching(_ point: CGPoint) -> (NSRange, Action)? {
269+
let text = adaptation(scaledAttributedText ?? synthesizedAttributedText ?? attributedText, with: numberOfLines)
270+
guard let attributedString = AttributedString(text) else { return nil }
271+
272+
// 构建同步Label的TextKit
273+
// 注: 目前还剩一个截断处理没解决 比如 "a\n\n\nb" numberOfLines=2
274+
let delegate = UILabelLayoutManagerDelegate(scaledMetrics)
275+
let textStorage = NSTextStorage()
276+
let textContainer = NSTextContainer(size: bounds.size)
277+
let layoutManager = NSLayoutManager()
278+
layoutManager.delegate = delegate // 重新计算行高确保TextKit与UILabel显示同步
279+
textContainer.lineBreakMode = lineBreakMode
280+
textContainer.lineFragmentPadding = 0.0
281+
textContainer.maximumNumberOfLines = numberOfLines
282+
layoutManager.usesFontLeading = false // UILabel没有使用FontLeading排版
283+
layoutManager.addTextContainer(textContainer)
284+
textStorage.addLayoutManager(layoutManager)
285+
textStorage.setAttributedString(attributedString.value) // 放在最后添加富文本 TextKit的坑
286+
287+
// 确保布局
288+
layoutManager.ensureLayout(for: textContainer)
289+
290+
// 获取文本所占高度
291+
let height = layoutManager.usedRect(for: textContainer).height
292+
293+
// 获取点击坐标 并排除各种偏移
294+
var point = point
295+
point.y -= (bounds.height - height) / 2
296+
297+
// Debug
298+
subviews.filter({ $0 is DebugView }).forEach({ $0.removeFromSuperview() })
299+
let view = DebugView(frame: .init(x: 0, y: (bounds.height - height) / 2, width: bounds.width, height: height))
300+
view.draw = { layoutManager.drawGlyphs(forGlyphRange: .init(location: 0, length: textStorage.length), at: .zero) }
301+
addSubview(view)
302+
303+
// 获取字形下标
304+
var fraction: CGFloat = 0
305+
let glyphIndex = layoutManager.glyphIndex(for: point, in: textContainer, fractionOfDistanceThroughGlyph: &fraction)
306+
// 获取字符下标
307+
let index = layoutManager.characterIndexForGlyph(at: glyphIndex)
308+
// 通过字形距离判断是否在字形范围内
309+
guard fraction > 0, fraction < 1 else {
310+
return nil
311+
}
312+
// 获取点击的字符串范围和回调事件
313+
guard
314+
let range = actions.keys.first(where: { $0.contains(index) }),
315+
let action = actions[range] else {
316+
return nil
317+
}
318+
return (range, action)
319+
}
320+
}
321+
322+
class DebugView: UIView {
323+
324+
var draw: (() -> Void)?
325+
326+
override init(frame: CGRect) {
327+
super.init(frame: frame)
328+
isUserInteractionEnabled = false
329+
backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.2983732877)
330+
}
331+
332+
required init?(coder: NSCoder) {
333+
fatalError("init(coder:) has not been implemented")
334+
}
335+
336+
override func draw(_ rect: CGRect) {
337+
super.draw(rect)
338+
self.draw?()
339+
}
340+
}
341+
257342
private extension String {
258343

259344
func reversedBase64Decoder() -> String? {
@@ -349,9 +434,9 @@ extension UILabel {
349434

350435
private func adaptation(_ string: NSAttributedString?, with numberOfLines: Int) -> NSAttributedString? {
351436
/**
352-
由于富文本中的lineBreakMode对于UILabel和TextKit的行为是不一致的, UILabel默认的.byTruncatingTail在TextKit中则无法正确显示.
353-
所以将富文本中的lineBreakMode全部替换为TextKit默认的.byWordWrapping, 以解决多行显示和不一致的问题.
354-
注意: 经测试 最大行数为1行时 换行模式表现与byCharWrapping一致.
437+
由于富文本中的lineBreakMode对于UILabel和TextKit的行为是不一致的, UILabel默认的.byTruncatingTail在TextKit中则无法正确显示.
438+
所以将富文本中的lineBreakMode全部替换为TextKit默认的.byWordWrapping, 以解决多行显示和不一致的问题.
439+
注意: 经测试 最大行数为1行时 换行模式表现与byCharWrapping一致.
355440
*/
356441
guard let string = string else {
357442
return nil
@@ -375,89 +460,5 @@ extension UILabel {
375460
}
376461
}
377462

378-
fileprivate extension UILabel {
379-
380-
@objc
381-
func attributedAction(_ sender: UIGestureRecognizer) {
382-
guard isActionEnabled else { return }
383-
guard let action = touched?.2 else { return }
384-
guard action.trigger.matching(sender) else { return }
385-
// 点击 回调
386-
action.handle?()
387-
}
388-
389-
func matching(_ point: CGPoint) -> (NSRange, Action)? {
390-
let text = adaptation(scaledAttributedText ?? synthesizedAttributedText ?? attributedText, with: numberOfLines)
391-
guard let attributedString = AttributedString(text) else { return nil }
392-
393-
// 构建同步Label设置的TextKit
394-
let delegate = UILabelLayoutManagerDelegate(scaledMetrics)
395-
let textStorage = NSTextStorage()
396-
let textContainer = NSTextContainer(size: .init(bounds.size.width, bounds.size.height))
397-
let layoutManager = NSLayoutManager()
398-
layoutManager.delegate = delegate // 重新计算行高确保TextKit与UILabel显示同步
399-
textContainer.lineBreakMode = lineBreakMode
400-
textContainer.lineFragmentPadding = 0.0
401-
textContainer.maximumNumberOfLines = numberOfLines
402-
layoutManager.usesFontLeading = false // UILabel没有使用FontLeading排版
403-
layoutManager.addTextContainer(textContainer)
404-
textStorage.addLayoutManager(layoutManager)
405-
textStorage.setAttributedString(attributedString.value) // 放在最后添加富文本 TextKit的坑
406-
407-
// 确保布局
408-
layoutManager.ensureLayout(for: textContainer)
409-
410-
// 获取文本所占高度
411-
let height = layoutManager.usedRect(for: textContainer).height
412-
413-
// 获取点击坐标 并排除各种偏移
414-
var point = point
415-
point.y -= (bounds.height - height) / 2
416-
417-
// Debug
418-
subviews.filter({ $0 is DebugView }).forEach({ $0.removeFromSuperview() })
419-
let view = DebugView(frame: .init(x: 0, y: (bounds.height - height) / 2, width: bounds.width, height: height))
420-
view.draw = { layoutManager.drawGlyphs(forGlyphRange: .init(location: 0, length: textStorage.length), at: .zero) }
421-
addSubview(view)
422-
423-
// 获取字形下标
424-
var fraction: CGFloat = 0
425-
let glyphIndex = layoutManager.glyphIndex(for: point, in: textContainer, fractionOfDistanceThroughGlyph: &fraction)
426-
// 获取字符下标
427-
let index = layoutManager.characterIndexForGlyph(at: glyphIndex)
428-
// 通过字形距离判断是否在字形范围内
429-
guard fraction > 0, fraction < 1 else {
430-
return nil
431-
}
432-
// 获取点击的字符串范围和回调事件
433-
guard
434-
let range = actions.keys.first(where: { $0.contains(index) }),
435-
let action = actions[range] else {
436-
return nil
437-
}
438-
return (range, action)
439-
}
440-
}
441-
442-
class DebugView: UIView {
443-
var draw: (() -> Void)?
444-
445-
override init(frame: CGRect) {
446-
super.init(frame: frame)
447-
isUserInteractionEnabled = false
448-
// backgroundColor = .clear
449-
backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.2983732877)
450-
}
451-
452-
required init?(coder: NSCoder) {
453-
fatalError("init(coder:) has not been implemented")
454-
}
455-
456-
override func draw(_ rect: CGRect) {
457-
super.draw(rect)
458-
self.draw?()
459-
}
460-
}
461-
462463
#endif
463464
#endif

Sources/Extension/UIKit/UILabel/UILabelLayoutManagerDelegate.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
// Copyright © 2020 LEE. All rights reserved.
1212
//
1313

14-
#if os(iOS) || os(tvOS)
14+
#if os(iOS)
1515

1616
import UIKit
1717

1818
class UILabelLayoutManagerDelegate: NSObject, NSLayoutManagerDelegate {
1919

20-
// 当Label发生Scalet时 最大行数为1时 基线偏移不会改变
20+
// 当Label发生Scaled时
2121
let scaledMetrics: UILabel.ScaledMetrics?
2222

2323
init(_ scaledMetrics: UILabel.ScaledMetrics?) {
@@ -63,19 +63,19 @@ class UILabelLayoutManagerDelegate: NSObject, NSLayoutManagerDelegate {
6363

6464
var rect = lineFragmentRect.pointee
6565
var used = lineFragmentUsedRect.pointee
66-
// 以最大的高度为准 (可解决附件问题), 同时根据最大行数是否为1来判断rect和used是否需要一致, 以解决1行数多余的行间距问题.
66+
// 以最大的高度为准 (可解决附件问题), 同时根据最大行数是否为1来判断used是否需要增加行间距, 以解决1行时应该无行间距的问题.
6767
let temp = max(maximum.lineHeight, used.height)
6868
rect.size.height = temp + maximum.lineSpacing + paragraphSpacing + paragraphSpacingBefore
69-
used.size.height = textContainer.maximumNumberOfLines == 1 ? temp : rect.height
69+
used.size.height = textContainer.maximumNumberOfLines == 1 ? temp : temp + maximum.lineSpacing
7070

71-
// 当Label发生Scaled时 最大行数为1时 基线偏移不会改变
71+
// 当Label发生Scaled时 最大行数为1时 基线偏移不会按比例计算
7272
if let scaledMetrics = scaledMetrics, textContainer.maximumNumberOfLines == 1 {
73+
// 使用原始基线偏移 使用Scaled的尺寸高度
7374
var baseline = baselineOffset.pointee
74-
let cha = CGFloat(scaledMetrics.baselineOffset - scaledMetrics.baselineOffset * scaledMetrics.actualScaleFactor)
75-
baseline += cha
75+
baseline = CGFloat(scaledMetrics.baselineOffset)
7676
baselineOffset.pointee = baseline
77-
rect.size.height += cha
78-
used.size.height += cha
77+
rect.size.height = scaledMetrics.scaledSize.height
78+
used.size.height = scaledMetrics.scaledSize.height
7979
}
8080

8181
// 重新赋值最终结果
@@ -163,7 +163,7 @@ extension UILabelLayoutManagerDelegate {
163163
}
164164
}
165165

166-
extension NSAttributedString.Key {
166+
fileprivate extension NSAttributedString.Key {
167167

168168
/// 参考: https://juejin.im/post/6844903838252531725
169169
static let originalFont: NSAttributedString.Key = .init("NSOriginalFont")

0 commit comments

Comments
 (0)