1. 邏輯分析
- 素材就是一些emoji 的字符串和一些表情圖片資源, 打包成Emoticons.bundle放在demo中了.
- 可以先將demo 下載下來, 對(duì)照bundle 文件更好理解.
/* 自定義表情鍵盤
- 表情數(shù)據(jù)結(jié)果分析
- 分為幾種表情(4種)
- 最近 (1頁表情, 20個(gè))
- [20]
- 默認(rèn) (6頁表情, 108個(gè))
- [20] [20] [20] [20] [20] [8]
- emoji (4頁表情, 80個(gè))
- [20] [20] [20] [20]
- lxh (2頁表情, 40個(gè))
- [20] [20]
- 分析collectionView 怎么顯示
- 最近
- 1 頁
- 默認(rèn)
- 6 頁
- emoji
- 4 頁
- lxh
- 2 頁
? - 怎么確定collectionView 的組數(shù)?
- [[[20]], [[20] [20] [20] [20] [20] [8]], [[20] [20] [20] [20]], [[20] [20]]].count = 4組.
- 也就是section 數(shù)量 = 三維數(shù)組.count
- 每一個(gè)section 的item 數(shù)量 = 三維數(shù)組[單個(gè)元素].count(也就是 二維數(shù)組.count).
- 而每一個(gè)item 上有20 個(gè)表情控件.
*/
2. 層級(jí)結(jié)構(gòu)分析(由于Xcode渲染不出來鍵盤, 所以在文末有一張靈魂手繪, 希望能對(duì)理解歷來有所幫助吧.)
2.1 圖層結(jié)構(gòu)分析
- 第一層級(jí) viewController.view
- 第二層級(jí)
- UITextView (系統(tǒng)鍵盤)
- 笑臉View (目的: 點(diǎn)擊切換鍵盤)
- 第三層級(jí)
- 自定義的inputView, 來替換系統(tǒng)的鍵盤 (也就是自定義的表情鍵盤, 替換UITextView 的系統(tǒng)鍵盤)
- 自定義的inputView 有哪些子控件呢?
- UICollectionView, 承載那些小表情
- 分組的View, 代表每一大組
2.2 自定義的inputView 中UICollectionView 的分析
/* inputView 的UICollectionView
- 組數(shù)代表有多少不同款的表情
- 款式數(shù)的總量, 就是底部分組欄 有多少個(gè)分組的按鈕(最近类早、默認(rèn)、emoji骂铁、等等)
- 每一個(gè)item 的size 都是inputView 的寬度, 高度 = inputView - 底部分組欄的高度
- 每一個(gè)item 上面可承載(七列三行) 7*3 - 1(刪除按鈕) = 20 個(gè)button
- 因此, 每一種類別擁有所有表情的個(gè)數(shù) / 20 = 每一組item 的數(shù)量
*/
3. 代碼實(shí)現(xiàn)
3.1 切換系統(tǒng)鍵盤和自定義的鍵盤
/* UITextView 自帶的inputView 屬性, 可以重新賦值來切換
- customTextView 為自定義的UITextView
- emoticonKeyboardView 為自己自定義的表情鍵盤, 繼承自UIView
*/
func switchKeyboard(){
// 如果inputView == nil 就代表是系統(tǒng)鍵盤 改成自定義鍵盤
if self.customTextView.inputView == nil {
self.customTextView.inputView = self.emoticonKeyboardView
}else {
// 如果inputView != nil 就代表你設(shè)置了自定義鍵盤 改成系統(tǒng)鍵盤
self.customTextView.inputView = nil
}
// 開啟第一響應(yīng)
self.customTextView.becomeFirstResponder()
// 切換inputView后要刷新
self.customTextView.reloadInputViews()
}
3.2 獲取本地bundle文件, 加載其中的資源
- 因?yàn)榭赡苡玫芥I盤的地方較多, 避免每次調(diào)出鍵盤都去訪問磁盤資源, 這里做了一個(gè)單例, 將磁盤資源加載到緩存, 方便使用.
// 獲取表情bundle
lazy var emoticonBundle:Bundle = {
// 路徑
let path = Bundle.main.path(forResource: "Emoticons.bundle", ofType: nil)!
// 獲取bundle
let bundle = Bundle(path: path)!
return bundle
}()
// MARK: 根據(jù)bundle 文件中對(duì)應(yīng)的文件名稱不同, 獲取不同名稱下的資源
// 通過該方法分別獲取不同的表情包的 一維數(shù)組
func loadSingledimensionalEmoticonsArray(name: String) -> [HEmoticonModel]{
// 路徑
let file = emoticonBundle.path(forResource: "\(name)/info.plist", ofType: nil)!
// plist 數(shù)組
let plistArray = NSArray(contentsOfFile: file)!
// 創(chuàng)建臨時(shí)可變字典
var tempArray: [HEmoticonModel] = [HEmoticonModel]()
// 字典轉(zhuǎn)模型
for dict in plistArray {
let model = HEmoticonModel(dict: dict as! [String : Any])
// 給path 賦值
model.path = "\(name)" + "/" + "\(model.png ?? "")"
tempArray.append(model)
}
return tempArray
}
3.3 一維數(shù)組轉(zhuǎn)二維數(shù)組
// 通過該方法把一維數(shù)組轉(zhuǎn)成 二維數(shù)組
func loadTwoDimensionalEmoticonsArray(emoticons: [HEmoticonModel]) -> [[HEmoticonModel]]{
// 得到一維數(shù)組將要在表情鍵盤顯示的頁數(shù)
let pageCount = (emoticons.count + HEMOTICONMAXCOUNT - 1)/HEMOTICONMAXCOUNT
// 創(chuàng)建一個(gè)二維數(shù)組可變的 空數(shù)組
var groupArray: [[HEmoticonModel]] = [[HEmoticonModel]]()
for i in 0..<pageCount{
// 位置: 截取子數(shù)組的起始頁數(shù), 與HEMOTICONMAXCOUNT 做積, 代表從一位數(shù)組的那個(gè)位置開始截取
let loc = i * HEMOTICONMAXCOUNT
// 長(zhǎng)度: 將要截取的子數(shù)組的長(zhǎng)度
var len = HEMOTICONMAXCOUNT
// 反之越界
if len + loc > emoticons.count {
len = emoticons.count - loc
}
// 范圍
let range = NSRange(location: loc, length: len)
// 截取數(shù)組 -> NSArray 的方法, 通過起始位置和每次截取的數(shù)量, 來獲取子數(shù)組
let tempArray = (emoticons as NSArray).subarray(with: range) as! [HEmoticonModel]
// 添加到二維數(shù)組中
groupArray.append(tempArray)
}
// 返回
return groupArray
}
3.4 UICollectionView 數(shù)據(jù)源方法的return 值
func numberOfSections(in collectionView: UICollectionView) -> Int {
// 三維數(shù)組.count 即為組數(shù)
return HEmoticonTools.shared.allEmoticons.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// 二維數(shù)組.count 即為item數(shù)
return HEmoticonTools.shared.allEmoticons[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: pageViewCellId, for: indexPath) as! HEmoticonPageViewCell
cell.indexPath = indexPath // 測(cè)試使用
// 賦值 -> 一維數(shù)組賦值
cell.emoticons = HEmoticonTools.shared.allEmoticons[indexPath.section][indexPath.item]
return cell
}
4. '最近使用' 分組表情和表情上屏分析
- '最近使用' 分組的數(shù)據(jù)是記錄當(dāng)前用戶最近使用過的表情, 存入了沙盒, 實(shí)時(shí)刷新.
// 保存表情模型 -> 為 最近表情 提供數(shù)據(jù)
func saveRecentModel(emoticonModel: HEmoticonModel){
// 遍歷當(dāng)前的最近表情的數(shù)組 -> 去重 -> 有一樣的先移除, 然后添加到最后
for (i, model) in recentEmoticons.enumerated() {
// 判斷你的類型 emoji 還是 圖片
if model.isEmoji {
// emoji
if model.code == emoticonModel.code {
recentEmoticons.remove(at: i)
}
}else {
//圖標(biāo)表情
if model.png == emoticonModel.png {
recentEmoticons.remove(at: i)
}
}
}
// 添加到最近表情數(shù)組中
recentEmoticons.insert(emoticonModel, at: 0)
// 判斷如果超過20個(gè) 干掉最后一個(gè)
if recentEmoticons.count > 20 {
recentEmoticons.removeLast()
}
// 三維數(shù)組中的最近表情的數(shù)組進(jìn)行更改
allEmoticons[0][0] = recentEmoticons
// 保存到沙盒中
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: recentEmoticons, requiringSecureCoding: false)
do {
_ = try data.write(to: URL(fileURLWithPath: file))
print("最近表情寫入成功")
} catch {
print("最近表情寫入本地失敗: \(error)")
}
} catch {
}
}
- 表情上屏是用的原生富文本實(shí)現(xiàn)的, 直接賦值富文本屬性即可.
附知識(shí)點(diǎn)二:
.End