适用需求
在UILabel中展示自定义emoji或者gif/apng动画emoji,类似QQ聊天效果
背景介绍
使用NSAttributeString
和NSTextAttachment
可以在UILabel
中进行图文混排,因此可以把一张emoji图片设置为NSTextAttachment.image
或者.data
:
let testAttachment = NSTextAttachment()
testAttachment.image = UIImage(named: <#T##String#>)
let testAttributeString = NSAttributedString(attachment:testAttachment)
let testLabel = UILabel()
testLabel.attributedText = testAttributeString
但是这个方法不能展示gif等序列图动画,因此我去研究了一下大名鼎鼎的YYText
,bingo!完美解决!!!而且对YYText
的强大功能和简洁的代码崇拜不已。But,杀鸡焉用牛刀。因此,在参考YYText
的基础上,我自己写了一个满足需求的简易的EmojiLabel
。
正文
- 从gif里面获取每一帧图片
private func gifInfo(data: Data) -> [GifFrame] { let source = CGImageSourceCreateWithData(data as CFData, nil) let count = CGImageSourceGetCount(source!) if count == 1 { if let image = UIImage(data: data) { return [GifFrame(image: image, delayTime: 0)] } else { return [GifFrame(image: UIImage(), delayTime: 0)] } } var gifFrames = [GifFrame]() for i in 0..<count { autoreleasepool { let itemProperties = CGImageSourceCopyPropertiesAtIndex(source!, i, nil) as! [String: Any] let gifProperty = itemProperties[kCGImagePropertyGIFDictionary as String] as! [String: Any] let delayTimeUncalmpedProp = gifProperty[kCGImagePropertyGIFUnclampedDelayTime as String] as! Float if let cgImage = CGImageSourceCreateImageAtIndex(source!, i, nil) { let image = UIImage(cgImage: cgImage) let gifFrame = GifFrame(image: image, delayTime: delayTimeUncalmpedProp) gifFrames.append(gifFrame) } } } return gifFrames }
- 怎么让图片动起来?
前面已经说过,使用
NSTextAttachment
可以展示图片并且可以从gif序列里面把每一帧图片取出来,再加个定时器来不断的刷新图片
Swift tick = CADisplayLink(target: _AnimatedAttachmentWeakProxy(target: self), selector: #selector(step)) tick.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
Swift @objc private func step() { if index >= gifFrames.count { index = 0 } let curDelayTime = gifFrames[index].delayTime time += tick.duration self.image = gifFrames[index].image if time >= TimeInterval(curDelayTime) { time = 0 index += 1 } display?() }
Swift attachment.display = { [weak self] in self?.setNeedsDisplay() }
Swift override public var attributedText: NSAttributedString? { didSet { if let attributedText = attributedText { attributedText.enumerateAttribute(.attachment, in: NSMakeRange(0, attributedText.length), options: .reverse) { (obj, range, stop) in if let attachment = obj as? AnimatedAttachment { attachment.display = { [weak self] in self?.setNeedsDisplay() } } } } } }
前两段代码是在AnimatedAttachment
里面加一个定时器来遍历gif序列并且回调AttachmentLabel
;第三段是AttachmentLabel
中刷新;第四段是当AttachmentLabel
的attributedText
变化时向AnimatedAttachment
注册回调 - 解析收到的内容 EmoticonParser
在这个过程中,使用正则表达式来获取@objc func parser(text: String) -> NSAttributedString? { let attributeString = NSMutableAttributedString(string: text) do { var flags = [String]() for item in items { flags.append(item.flag) } if flags.isEmpty == true { return attributeString } let pattern = flags.joined(separator: "|") let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive) var results = [NSTextCheckingResult]() regex.enumerateMatches(in: text, options: NSRegularExpression.MatchingOptions.reportProgress, range: NSMakeRange(0, text.count)) { (result, flag, stop) in guard let result = result else { return } results.append(result) } if results.isEmpty == true { return attributeString } for result in results.reversed() { autoreleasepool { let fromIndex = text.index(text.startIndex, offsetBy: result.range.lowerBound) let toIndex = text.index(text.startIndex, offsetBy: result.range.lowerBound + result.range.length) let attachmentFlagString = text[fromIndex..<toIndex] for item in items { if item.flag == attachmentFlagString { let attachment = AnimatedAttachment() attachment.contents = item.emoticon attachment.bounds = CGRect(x: 0, y: 0, width: item.size.width, height: item.size.height) let att = NSAttributedString(attachment: attachment) attributeString.replaceCharacters(in: NSMakeRange(result.range.lowerBound, result.range.length), with: att) } } } } return attributeString } catch { dPrint("error: \(error)") return attributeString } }
text
中的表情特殊符,注意:for 循环要倒叙进行否则会出现越界的问题
使用
整个的使用过程是这样的
let path = Bundle.main.path(forResource: "5@2x.apng", ofType: nil)
let data = try! Data.init(contentsOf: URL(fileURLWithPath: path!)
let path2 = Bundle.main.path(forResource: "0@2x.apng", ofType: nil)
let data2 = try! Data.init(contentsOf: URL(fileURLWithPath: path2!)
let path4 = Bundle.main.path(forResource: "Expression_1@2x.png"ofType: nil)
let data4 = try! Data.init(contentsOf: URL(fileURLWithPath: path4!)
let item1 = EmoticonItem(flag: ":0:", emoticon: data, size: CGSi(width: 50, height: 50))
let item2 = EmoticonItem(flag: ":1:", emoticon: data2)
let item4 = EmoticonItem(flag: ":3:", emoticon: data4)
let parser = EmoticonParser(items: [item1, item2, item4]
let label = AttachmentLabel(frame: CGRect(x: 30, y: 30, width: 200height: 200))
label.backgroundColor = .gray
label.numberOfLines = 0
label.lineBreakMode = .byCharWrapping
view.addSubview(label
label.parser = parser
label.text = "是的护发素电:0:话费是的咖啡机啊但是jskdfja独立开:1:发绝对是计费可拉伸的就开放"
label.textColor = .white
我的个人博客
欢迎访问www.iosprogrammer.tech
谢谢!!!