Skip to content

Instantly share code, notes, and snippets.

@acalism
Created March 27, 2018 11:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save acalism/74768541fed266b414f036ab777800c2 to your computer and use it in GitHub Desktop.
Save acalism/74768541fed266b414f036ab777800c2 to your computer and use it in GitHub Desktop.
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