Created
March 27, 2018 11:37
-
-
Save acalism/74768541fed266b414f036ab777800c2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private let kEllipsesCharacter = "\u{2026}" | |
/// This componet is designed to resolve those problems: | |
/// 1. Calculate frame. | |
/// 2. TouchableLinks. | |
/// 3. Line/Height limits and followed by "..."/"...全文"/... . | |
class BKLabel: UIView { | |
/// 文本内容 | |
var attributedText: NSAttributedString? { | |
didSet { | |
if attributedText != oldValue { | |
reset() | |
} | |
} | |
} | |
/// 限制行数 | |
var limitNumberOfLines = 0 { | |
didSet { | |
if limitNumberOfLines != oldValue { | |
reset() | |
} | |
} | |
} | |
/// 限制宽高 | |
var limitWidth = CGFloat.greatestFiniteMagnitude { | |
didSet { | |
if limitWidth != oldValue { | |
reset() | |
} | |
} | |
} | |
var limitHeight = CGFloat.greatestFiniteMagnitude { | |
didSet { | |
if limitHeight != oldValue { | |
reset() | |
} | |
} | |
} | |
/// 尾部截断文案, 默认"..." | |
var truncatedAttributedString: NSAttributedString? { | |
didSet { | |
if truncatedAttributedString != oldValue { | |
reset() | |
} | |
} | |
} | |
/// 点击链接以及点击内容的回调 | |
/// 倘若不设置回调, 则控件不响应点击事件, 往上透传 | |
var clickLinkCallBack: ((URL) -> ())? | |
var clickContentCallBack: (() -> ())? | |
private(set) var numberOfLines = 0 | |
private(set) var maxNumberOfLines = 0 | |
private var lines: [CTLine]? | |
private var lineOrigins: [CGPoint]? | |
private var calculateSize: CGSize? | |
fileprivate lazy var tapGestureRecognizer: UITapGestureRecognizer = { | |
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapLabel(_:))) | |
gestureRecognizer.cancelsTouchesInView = false | |
gestureRecognizer.delaysTouchesBegan = false | |
gestureRecognizer.delaysTouchesEnded = false | |
return gestureRecognizer | |
}() | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
backgroundColor = .white | |
addGestureRecognizer(tapGestureRecognizer) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func draw(_ rect: CGRect) { | |
if lines == nil { | |
calculateLines() | |
} | |
if let lines = lines, let lineOrigins = lineOrigins, let context = UIGraphicsGetCurrentContext() { | |
context.textMatrix = CGAffineTransform.identity | |
context.translateBy(x: 0, y: size.height) | |
context.scaleBy(x: 1.0, y: -1.0) | |
lines.enumerated().forEach({ (index, line) in | |
guard let position = lineOrigins[safe: index] else { return } | |
context.textPosition = position | |
CTLineDraw(line, context) | |
}) | |
} | |
} | |
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { | |
if let attributes = attributesFromPoint(point) { | |
if let _ = attributes[NSAttributedStringKey.link] { | |
return self | |
} | |
} | |
if let _ = clickContentCallBack { | |
return self | |
} else { | |
return nil | |
} | |
} | |
} | |
extension BKLabel { | |
override var intrinsicContentSize: CGSize { | |
if let calculateSize = calculateSize { | |
return CGSize(width: nearbyint(calculateSize.width * 2) / 2, height: nearbyint(calculateSize.height * 2) / 2) | |
} else { | |
calculateLines() | |
if let calculateSize = calculateSize { | |
return CGSize(width: nearbyint(calculateSize.width * 2) / 2, height: nearbyint(calculateSize.height * 2) / 2) | |
} else { | |
return .zero | |
} | |
} | |
} | |
@objc func didTapLabel(_ gestureRecognizer: UITapGestureRecognizer) { | |
if let attributes = attributesFromPoint(gestureRecognizer.location(in: self)) { | |
if let url = attributes[NSAttributedStringKey.link] as? URL { | |
clickLinkCallBack?(url) | |
return | |
} else if let urlString = attributes[NSAttributedStringKey.link] as? String, let url = URL(string: urlString) { | |
clickLinkCallBack?(url) | |
return | |
} | |
} | |
clickContentCallBack?() | |
} | |
private func attributesFromPoint(_ point: CGPoint) -> [NSAttributedStringKey: Any]? { | |
guard let lines = lines, let calculateSize = calculateSize, let attributedText = attributedText else { | |
return nil | |
} | |
for (index, line) in lines.enumerated() { | |
guard let origin = lineOrigins?[safe: index] else { | |
break | |
} | |
let lineSize = CTLineGetBoundsWithOptions(line, CTLineBoundsOptions(rawValue: 0)).size | |
let lineRect = CGRect(origin: CGPoint(x: origin.x, y: calculateSize.height - origin.y - lineSize.height), size: lineSize) | |
if lineRect.contains(point) { | |
let convertPoint = CGPoint(x: point.x - lineRect.minX, y: point.y - lineRect.minY) | |
let index = CTLineGetStringIndexForPosition(line, convertPoint) | |
return attributedText.attributes(at: max(0, min(index, attributedText.length - 1)), effectiveRange: nil) | |
} | |
} | |
return nil | |
} | |
} | |
extension BKLabel { | |
private func reset() { | |
numberOfLines = 0 | |
maxNumberOfLines = 0 | |
lines = nil | |
lineOrigins = nil | |
calculateSize = nil | |
} | |
private func calculateLines() { | |
guard let attributedText = attributedText else { | |
return | |
} | |
let frameSetter = CTFramesetterCreateWithAttributedString(attributedText) | |
let limitSize = CGSize(width: limitWidth, height: limitHeight) | |
let drawSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0,0), nil, limitSize, nil) | |
let frame = calculateFrame(drawSize, frameSetter: frameSetter) | |
let lines = CTFrameGetLines(frame) as! [CTLine] | |
maxNumberOfLines = lines.count | |
var lineOrigins = Array.init(repeating: CGPoint.zero, count: maxNumberOfLines) | |
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins) | |
var intrinsicContentHeight: CGFloat = 0 | |
self.lines = lines.prefix(limitNumberOfLines > 0 ? limitNumberOfLines : lines.count).enumerated().filter({ (index, line) -> Bool in | |
guard let origin = lineOrigins[safe: index] else { return false } | |
let lineRect = CTLineGetBoundsWithOptions(line, CTLineBoundsOptions(rawValue: 0)) | |
if drawSize.height - origin.y + lineRect.height < limitHeight { | |
intrinsicContentHeight = drawSize.height - origin.y + lineRect.height | |
return true | |
} | |
return false | |
}).map { $0.1 } | |
numberOfLines = self.lines?.count ?? 0 | |
if let lastLine = self.lines?.last { | |
var lineAscent: CGFloat = 0 | |
CTLineGetTypographicBounds(lastLine, &lineAscent, nil, nil); | |
intrinsicContentHeight -= lineAscent | |
} | |
calculateSize = CGSize(width: drawSize.width, height: intrinsicContentHeight) | |
self.lineOrigins = lineOrigins.map({ (point) -> CGPoint in | |
return CGPoint(x: point.x, y: intrinsicContentHeight - (drawSize.height - point.y)) | |
}) | |
if let lastLine = self.lines?.last { | |
let stringRange = CTLineGetStringRange(lastLine) | |
if stringRange.location + stringRange.length < attributedText.length { | |
let truncatedString: NSAttributedString | |
if let truncatedAttributedString = truncatedAttributedString { | |
truncatedString = truncatedAttributedString | |
} else { | |
let attributs = attributedText.attributes(at: stringRange.location + stringRange.length, effectiveRange: nil) | |
truncatedString = NSAttributedString(string: kEllipsesCharacter, attributes: attributs) | |
} | |
let truncatedSize = truncatedString.size() | |
let index = CTLineGetStringIndexForPosition(lastLine, CGPoint(x: limitWidth - truncatedSize.width, y: drawSize.height / 2)) | |
let displayAttributedString = attributedText.attributedSubstring(from: NSMakeRange(stringRange.location, index - stringRange.location)).appending(truncatedString) | |
let truncatedLine = CTLineCreateWithAttributedString(displayAttributedString) | |
self.lines?.removeLast() | |
self.lines?.append(truncatedLine) | |
} | |
} | |
setNeedsDisplay() | |
} | |
private func calculateFrame(_ drawSize: CGSize, frameSetter: CTFramesetter) -> CTFrame { | |
let path = CGMutablePath() | |
path.addRect(CGRect(origin: .zero, size: drawSize)) | |
return CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment