1. 逻辑分析
- 素材就是一些emoji 的字符串和一些表情图片资源, 打包成Emoticons.bundle放在demo中了.
- 可以先将demo 下载下来, 对照bundle 文件更好理解.
/* 自定义表情键盘
- 表情数据结果分析
- 分为几种表情(4种)
- 最近 (1页表情, 20个)
- [20]
- 默认 (6页表情, 108个)
- [20] [20] [20] [20] [20] [8]
- emoji (4页表情, 80个)
- [20] [20] [20] [20]
- lxh (2页表情, 40个)
- [20] [20]
- 分析collectionView 怎么显示
- 最近
- 1 页
- 默认
- 6 页
- emoji
- 4 页
- lxh
- 2 页
? - 怎么确定collectionView 的组数?
- [[[20]], [[20] [20] [20] [20] [20] [8]], [[20] [20] [20] [20]], [[20] [20]]].count = 4组.
- 也就是section 数量 = 三维数组.count
- 每一个section 的item 数量 = 三维数组[单个元素].count(也就是 二维数组.count).
- 而每一个item 上有20 个表情控件.
*/
2. 层级结构分析(由于Xcode渲染不出来键盘, 所以在文末有一张灵魂手绘, 希望能对理解历来有所帮助吧.)
2.1 图层结构分析
- 第一层级 viewController.view
- 第二层级
- UITextView (系统键盘)
- 笑脸View (目的: 点击切换键盘)
- 第三层级
- 自定义的inputView, 来替换系统的键盘 (也就是自定义的表情键盘, 替换UITextView 的系统键盘)
- 自定义的inputView 有哪些子控件呢?
- UICollectionView, 承载那些小表情
- 分组的View, 代表每一大组
2.2 自定义的inputView 中UICollectionView 的分析
/* inputView 的UICollectionView
- 组数代表有多少不同款的表情
- 款式数的总量, 就是底部分组栏 有多少个分组的按钮(最近、默认、emoji、等等)
- 每一个item 的size 都是inputView 的宽度, 高度 = inputView - 底部分组栏的高度
- 每一个item 上面可承载(七列三行) 7*3 - 1(删除按钮) = 20 个button
- 因此, 每一种类别拥有所有表情的个数 / 20 = 每一组item 的数量
*/
3. 代码实现
文件夹说明
3.1 切换系统键盘和自定义的键盘
/* UITextView 自带的inputView 属性, 可以重新赋值来切换
- customTextView 为自定义的UITextView
- emoticonKeyboardView 为自己自定义的表情键盘, 继承自UIView
*/
func switchKeyboard(){
// 如果inputView == nil 就代表是系统键盘 改成自定义键盘
if self.customTextView.inputView == nil {
self.customTextView.inputView = self.emoticonKeyboardView
}else {
// 如果inputView != nil 就代表你设置了自定义键盘 改成系统键盘
self.customTextView.inputView = nil
}
// 开启第一响应
self.customTextView.becomeFirstResponder()
// 切换inputView后要刷新
self.customTextView.reloadInputViews()
}
3.2 获取本地bundle文件, 加载其中的资源
- 因为可能用到键盘的地方较多, 避免每次调出键盘都去访问磁盘资源, 这里做了一个单例, 将磁盘资源加载到缓存, 方便使用.
// 获取表情bundle
lazy var emoticonBundle:Bundle = {
// 路径
let path = Bundle.main.path(forResource: "Emoticons.bundle", ofType: nil)!
// 获取bundle
let bundle = Bundle(path: path)!
return bundle
}()
// MARK: 根据bundle 文件中对应的文件名称不同, 获取不同名称下的资源
// 通过该方法分别获取不同的表情包的 一维数组
func loadSingledimensionalEmoticonsArray(name: String) -> [HEmoticonModel]{
// 路径
let file = emoticonBundle.path(forResource: "\(name)/info.plist", ofType: nil)!
// plist 数组
let plistArray = NSArray(contentsOfFile: file)!
// 创建临时可变字典
var tempArray: [HEmoticonModel] = [HEmoticonModel]()
// 字典转模型
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 一维数组转二维数组
// 通过该方法把一维数组转成 二维数组
func loadTwoDimensionalEmoticonsArray(emoticons: [HEmoticonModel]) -> [[HEmoticonModel]]{
// 得到一维数组将要在表情键盘显示的页数
let pageCount = (emoticons.count + HEMOTICONMAXCOUNT - 1)/HEMOTICONMAXCOUNT
// 创建一个二维数组可变的 空数组
var groupArray: [[HEmoticonModel]] = [[HEmoticonModel]]()
for i in 0..<pageCount{
// 位置: 截取子数组的起始页数, 与HEMOTICONMAXCOUNT 做积, 代表从一位数组的那个位置开始截取
let loc = i * HEMOTICONMAXCOUNT
// 长度: 将要截取的子数组的长度
var len = HEMOTICONMAXCOUNT
// 反之越界
if len + loc > emoticons.count {
len = emoticons.count - loc
}
// 范围
let range = NSRange(location: loc, length: len)
// 截取数组 -> NSArray 的方法, 通过起始位置和每次截取的数量, 来获取子数组
let tempArray = (emoticons as NSArray).subarray(with: range) as! [HEmoticonModel]
// 添加到二维数组中
groupArray.append(tempArray)
}
// 返回
return groupArray
}
3.4 UICollectionView 数据源方法的return 值
func numberOfSections(in collectionView: UICollectionView) -> Int {
// 三维数组.count 即为组数
return HEmoticonTools.shared.allEmoticons.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// 二维数组.count 即为item数
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 // 测试使用
// 赋值 -> 一维数组赋值
cell.emoticons = HEmoticonTools.shared.allEmoticons[indexPath.section][indexPath.item]
return cell
}
4. '最近使用' 分组表情和表情上屏分析
- '最近使用' 分组的数据是记录当前用户最近使用过的表情, 存入了沙盒, 实时刷新.
// 保存表情模型 -> 为 最近表情 提供数据
func saveRecentModel(emoticonModel: HEmoticonModel){
// 遍历当前的最近表情的数组 -> 去重 -> 有一样的先移除, 然后添加到最后
for (i, model) in recentEmoticons.enumerated() {
// 判断你的类型 emoji 还是 图片
if model.isEmoji {
// emoji
if model.code == emoticonModel.code {
recentEmoticons.remove(at: i)
}
}else {
//图标表情
if model.png == emoticonModel.png {
recentEmoticons.remove(at: i)
}
}
}
// 添加到最近表情数组中
recentEmoticons.insert(emoticonModel, at: 0)
// 判断如果超过20个 干掉最后一个
if recentEmoticons.count > 20 {
recentEmoticons.removeLast()
}
// 三维数组中的最近表情的数组进行更改
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 {
}
}
- 表情上屏是用的原生富文本实现的, 直接赋值富文本属性即可.
附知识点二: 层级关系
.End
网友评论