前言
最近换了一个工作,有点忙,记录笔记的时间越来越少了。关于RxSwift专栏的文章,笔者后续再抽时间记录。今天主要分享一个笔者碰到的一个需求,在实现的过程中决定封装一下,希望可以帮到有需要的小伙伴。\
需求:实现红色框框内的效果
要求:
- 按钮支持点击选中和反选
- 协议支持点击
- 支持换行
- 截断时也要支持点击
- 图文中心保持一致
分析:
前3点不难,用按钮label组合一下即可,后面两点就要求必须使用富文本了,因为截断的情况不固定,比如:
或者
这两种都不好处理,用按钮无法实现截断的处理。
实现
处理图片
图片的比较容易处理一些,将图片转成富文本,和后面内容拼接即可,这里需要注意的是图片的中心点要和后面的文案对齐,对齐可以采用基线偏移来处理(偏移的是后面的文案)
let baselineOffset = (largeFont.lineHeight - normalFont.lineHeight)/2.0 + (largeFont.descender - normalFont.descender)
muAttributedString.addAttributes([NSAttributedString.Key.font: normalFont,
NSAttributedString.Key.baselineOffset: baselineOffset],
range: NSRange(location: normalImgAttrString.length, length: muAttributedString.length - normalImgAttrString.length))
获取文案的区域
通常我们想的是直接使用这个方法来获取
layoutManager.boundingRect(forGlyphRange: , in: )
这个方法对于第一种情况是可以解决的,但是后面截断的情况无法处理。
翻看layoutManager的API发现有一个方法可以处理截断的情况
open func enumerateEnclosingRects(forGlyphRange glyphRange: NSRange, withinSelectedGlyphRange selectedRange: NSRange, in textContainer: NSTextContainer, using block: @escaping (CGRect, UnsafeMutablePointer<ObjCBool>) -> Void)
枚举textContainer中glyphRange的封闭矩形。如果在第二个参数中给出了所选范围,则返回的矩形将正确用于绘制所选内容。
从官方的注解可以发现,将给定范围的文字的所有区域枚举一遍。从而通过第一个矩形和最后一个矩形即可覆盖所有的文字范围,如下图:
实现代码:
func rectFor(string str : String, fromIndex: Int = 0) -> (CGRect, CGRect)?
{
// Find the range of the string
guard self.text != nil else { return nil }
let subStringToSearch : NSString = (self.text! as NSString).substring(from: fromIndex) as NSString
var stringRange = subStringToSearch.range(of: str)
if (stringRange.location != NSNotFound)
{
guard self.attributedText != nil else { return nil }
// Add the starting point to the sub string
stringRange.location += fromIndex
let storage = NSTextStorage(attributedString: self.attributedText!)
let layoutManager = NSLayoutManager()
storage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: self.frame.size)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = .byWordWrapping
layoutManager.addTextContainer(textContainer)
var glyphRange = NSRange()
layoutManager.characterRange(forGlyphRange: stringRange, actualGlyphRange: &glyphRange)
var firstWordRect = true
var rect1 = CGRectZero
var rect2 = CGRectZero
layoutManager.enumerateEnclosingRects(forGlyphRange: glyphRange, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 1), in: textContainer) { wordRect, isStop in
if firstWordRect {
rect1 = wordRect
firstWordRect = false
}
rect2 = wordRect
}
return (rect1, rect2)
}
return nil
}
点击处理
@objc
private func click(_ gesture: UITapGestureRecognizer) {
guard let dict = callBackMap else {
return
}
let point = gesture.location(in: self)
for key in dict.keys {
let (rect1, rect2) = rectFor(string: key)!
let imgString = selected ? selectedImgAttrString : normalImgAttrString
if containsPoint(minRect: rect1, maxRect: key == imgString.string ? CGRect(x: 0, y: 0, width: largeFont.pointSize, height: largeFont.pointSize):rect2, point: point) {
if key == imgString.string {
selected = !selected
}
if let callBack = dict[key] {
callBack()
}
}
}
}
总结
笔者封装了一个UILabel的扩展,可方便使用。完整的demo已放在github,有兴趣的小伙伴可以去下载用一下看看
注意事项:
图片和largefont的大小最好保持一致,不然基线偏移的时候要特殊处理,这种情况在代码里做了注解
更新
问题:
如果有多个重复的协议,第一个可以点击,后面的重复协议不支持点击
解决:
1.文本颜色:添加协议时,遍历所有文本,得到该字串的所有range,从而修改文本颜色。
2.点击处理:同样遍历字串的所有range,直到点击的位置落在range区域内,或者查询无果不做响应
阶段性总结
笔者在遇到问题时,会尽量的解决,也欢迎小伙伴把碰到的问题提出来,一起学习进步。
在此感谢小伙伴season_zhu的提示,demo链接已更新。
支持pod使用:
pod 'UILabelImageText'