廢話
在做表情鍵盤時(shí),很多時(shí)候?yàn)榱耸沟酶鱾€(gè)平臺(tái)的表情得到統(tǒng)一(或者是對表情的擴(kuò)展等等),因此采用自定義的圖片表情,而非系統(tǒng)自帶的表情。
前段時(shí)間剛好遇到這樣的需求, 因此打算動(dòng)手寫一個(gè)(造輪子), 粗糙的輪子成形之后,決定從以下幾個(gè)方面入手,做點(diǎn)小筆記。
先上圖 :
正文
<h3 id="0">1. 表情建模</h3>
表情建模,個(gè)人感覺是非常非常有必要的(畢竟一個(gè)表情包含的不僅僅是圖片),表情大概具有以下屬性:
- ID (對應(yīng)的表情包id)
- image (對應(yīng)的圖片)
- code (十六進(jìn)制編碼, 如:系統(tǒng)表情)
- isDelete (是不是刪除按鈕)
當(dāng)然,還有表情包的模型,不再贅述。
<h3 id="1">2. 自定義圖片表情的排版</h3>
表情布局方式很多,比如:UIScrollView,UICollectionView等等,這里選擇后者,別問為什么,兩個(gè)字: 簡單。
但是:在設(shè)置了scrollDirection = .Horizontal之后,UICollectionViewCell的布局就變成了從上到下,從左到右的形式。如圖:
這樣就會(huì)導(dǎo)致,在分頁滾動(dòng)時(shí), 最后一頁可能出現(xiàn)半頁的情況,既然已經(jīng)知道了問題是怎么導(dǎo)致的,那么解決這樣的問題也就簡單了,只需要重寫
UICollectionViewLayout 給 UICollectionViewCell 重新布局便可。
代碼如下:
class EmojiLayout: UICollectionViewLayout {
// 保存所有item屬性
private var attributes: [UICollectionViewLayoutAttributes] = []
// screen
private let mainRect = UIScreen.mainScreen().bounds
// section
private var sections: Int = 0
// item
private var items: Int = 0
// column
var maxColumn: CGFloat = 0
// row
var maxRow: CGFloat = 0
// margin
var margin: CGFloat = 0
// MARK: - 允許重新布局
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
// MARK: - 重新布局
override func prepareLayout() {
super.prepareLayout()
attributes.removeAll()
// 根據(jù)設(shè)置的Column Row, 計(jì)算得到每個(gè)item的大小
let itemsize = getItemSize(maxColumn, row: maxRow, margin: margin)
// 獲取組數(shù)
sections = self.collectionView?.numberOfSections() ?? 0
// 遍歷每組里面的所有item
for section in 0 ..< sections {
items = self.collectionView?.numberOfItemsInSection(section) ?? 0
// 遍歷每一個(gè)item
for item in 0 ..< items {
// 根據(jù) section, item 獲取每一個(gè)item的indexPath值
let indexPath = NSIndexPath(forItem: item, inSection: section)
// 根據(jù)indexPath值, 獲取每一個(gè)item的屬性
let attribute = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
// 通過一系列腦殘計(jì)算, 得到x, y值
let x = margin + (itemsize.width + margin) * (CGFloat(item) % maxColumn) + (CGFloat(section) * mainRect.width)
let y = margin + (itemsize.height + margin) * CGFloat(item / Int(maxColumn))
attribute.frame = CGRect(x: x, y: y, width: itemsize.width, height: itemsize.height)
// 把每一個(gè)新的屬性保存起來
attributes.append(attribute)
}
}
}
// MARK: - 返回當(dāng)前可見的
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var rectAttributes: [UICollectionViewLayoutAttributes] = []
// 遍歷所有屬性, 返回當(dāng)前處于可見區(qū)域的item(在屏幕可見的item)
let _ = attributes.map {
if CGRectContainsRect(rect, $0.frame) {
rectAttributes.append($0)
}
}
return rectAttributes
}
// MARK: - 返回大小
override func collectionViewContentSize() -> CGSize {
let itemsize = getItemSize(maxColumn, row: maxRow, margin: margin)
return CGSize(width: CGFloat(sections) * mainRect.width, height: margin + (maxRow * (itemsize.height + margin)))
}
// MARK: - itemSize
// 為了使得表情不變形, 因此 height = width
private func getItemSize(column: CGFloat, row: CGFloat, margin: CGFloat) -> CGSize {
let width = (mainRect.width - ((column + 1) * margin)) / column
return CGSize(width: width, height: width)
}
}
最后結(jié)果,如圖:
沒錯(cuò),就是這么簡單。
表情展示的時(shí)候,本人的做法:表情頁(每個(gè)section),通過計(jì)算得到,表情包能分成多少個(gè)section。切換表情包的時(shí)候,修改collectionView的datasource,然后reloadData
當(dāng)然,做法不是唯一的。
<h3 id="2">3. 在UITextView中插入表情</h3>
在UITextView中插入表情,分為2種情況:系統(tǒng)表情 和 圖片表情
1)系統(tǒng)表情
系統(tǒng)表情,其實(shí)就是字符串(個(gè)人感覺是字符串,不懂這樣理解對否),因此不需要我們做太多的操作,可以直接給UITextView的text屬性賦值。
2)圖片表情
圖片表情插入到 UITextView,可以通過 attributedText 屬性進(jìn)行設(shè)置(這幾乎是最簡單的方式法了)
主要2點(diǎn):
a. 為了使用方便,直接給 UITextView 擴(kuò)展方法 func insertEmoji(emojiModel: EmojiModel)
b. 繼承 NSTextAttachment 添加一個(gè)屬性 emojiTag 用于記錄該表情對應(yīng)的字符串
具體操作如下:
extension UITextView {
/// 插入表情
///
/// - parameter emojiModel: 表情模型
func insertEmoji(emojiModel: EmojiModel) {
// 刪除按鈕
if emojiModel.deleteBtn {
deleteBackward()
return
}
// 系統(tǒng)表情
if let emoji = emojiModel.emoji {
insertText(emoji)
}
// png 表情
if let pngImage = emojiModel.pngImage {
// 創(chuàng)建附件
let attachment = EmojiTextAttachment()
// 設(shè)置圖片
attachment.image = pngImage
// 設(shè)置圖片標(biāo)志(這里設(shè)置圖片標(biāo)志,主要是為了:表情轉(zhuǎn)換字符串時(shí) 操作更簡單)
attachment.emojiTag = emojiModel.chs
// 設(shè)置附件大小 (表情跟文本的大小一致)
attachment.bounds = CGRect(x: 0, y: -4, width: font!.lineHeight, height: font!.lineHeight)
// 帶屬性的文本, 把圖片設(shè)置進(jìn)去
let attritubeString = NSMutableAttributedString(attributedString: NSAttributedString(attachment: attachment))
// 設(shè)置字體
attritubeString.addAttribute(NSFontAttributeName, value: font!, range: NSRange(location: 0, length: 1))
// 獲取原來的文本, 替換為現(xiàn)在的文本, 再給textView的屬性文本賦值
let att = NSMutableAttributedString(attributedString: attributedText)
att.replaceCharactersInRange(self.selectedRange, withAttributedString: attritubeString)
attributedText = att
// 移動(dòng)光標(biāo)
selectedRange.location += 1
// 重寫通知, 代理方法
NSNotificationCenter.defaultCenter().postNotificationName(UITextViewTextDidChangeNotification, object: self)
delegate?.textViewDidChange!(self)
}
}
}
<h3 id="3">4. 表情轉(zhuǎn)換字符串</h3>
由于服務(wù)器無法識(shí)別 NSAttributedString 這樣的屬性文本,因此有必要把表情轉(zhuǎn)換成對應(yīng)的字符串。
既然圖片表情是通過 NSAttributedString 進(jìn)行設(shè)置的,那么同樣的道理,我們可以再通過遍歷屬性,找到圖片表情對應(yīng)的字符串。
如下:
extension NSAttributedString {
// 遍歷屬性, 獲取字符串
func getPlainString() -> String {
let plainStr = NSMutableString(string: self.string)
var base: Int = 0
self.enumerateAttribute(NSAttachmentAttributeName, inRange: NSMakeRange(0, self.length), options: []) { (value, range, stop) -> Void in
if let value = value as? EmojiTextAttachment {
if let emojiTag = value.emojiTag {
plainStr.replaceCharactersInRange(NSMakeRange(range.location + base, range.length), withString: emojiTag)
base += emojiTag.characters.count - 1
}
}
}
return plainStr as String
}
}
<h3 id="4">5. 字符串轉(zhuǎn)換表情</h3>
最后,可能有人覺得很奇怪,為什么要把表情轉(zhuǎn)換成字符串,然后字符串又要轉(zhuǎn)換成表情?
其實(shí),這兩者并不沖突,也不是多此一舉,而是很有必要的操作,為啥呢?
如圖:
字符串轉(zhuǎn)換表情,本人的做法很low,并不建議這么做,倘若你也沒有什么辦法的話,那就勉強(qiáng)看一下吧(前提是,你的表情對應(yīng)的字符串是這樣的:[哈哈] [玫瑰] [愛心])。
直接上代碼:
/// 符合"標(biāo)準(zhǔn)"的字符串
struct StringProperty {
/// 字符串
var value: String
/// 位置
var range: NSRange
init(value: String, range: NSRange) {
self.value = value
self.range = range
}
}
extension String {
/// 字符串查找, 返回以start開始 且 以end結(jié)尾的字符串
///
/// - parameter start: 開始字符串
/// - parameter end: 結(jié)束字符串
///
/// - returns: 如果存在, 返回 StringProperty 對象, 否則返回nil
func between(start: String, _ end: String) -> StringProperty? {
var range = NSRange()
var flag: Bool = false
let string = self as NSString
for var i = 0; i < string.length; i += 1 {
let subStr = string.substringWithRange(NSRange(location: i, length: 1))
if start == subStr {
range.location = i
flag = true
continue
} else if flag && subStr == end {
flag = false
range.length = i - range.location - 1
let strRange = NSRange(location: range.location, length: range.length + start.characters.count + end.characters.count)
let value = string.substringWithRange(strRange)
return StringProperty(value: value, range: strRange)
}
}
return nil
}
}
通過func between(start: String, _ end: String) -> StringProperty?方法可以獲取到“有可能是表情的字符串”,接下來就是要確定這個(gè)字符串是不是表情。本人也是用了很low的辦 —— “遍歷表情包”
具體做法,可以看demo中的EmojiPackageManager.swift文件下的class func verificationEmojiWithString(string: String) -> EmojiModel?方法。