iOS-自定義表情鍵盤

demo地址

效果圖

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)二:
層級(jí)關(guān)系

.End

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末坤学,一起剝皮案震驚了整個(gè)濱河市辖所,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖茫负,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件破停,死亡現(xiàn)場(chǎng)離奇詭異翅楼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)真慢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門毅臊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人黑界,你說我怎么就攤上這事管嬉。” “怎么了朗鸠?”我有些...
    開封第一講書人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵蚯撩,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我烛占,道長(zhǎng)胎挎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮犹菇,結(jié)果婚禮上德迹,老公的妹妹穿的比我還像新娘。我一直安慰自己项栏,他們只是感情好浦辨,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沼沈,像睡著了一般流酬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上列另,一...
    開封第一講書人閱讀 50,050評(píng)論 1 291
  • 那天芽腾,我揣著相機(jī)與錄音,去河邊找鬼页衙。 笑死摊滔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的店乐。 我是一名探鬼主播艰躺,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼眨八!你這毒婦竟也來了腺兴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤廉侧,失蹤者是張志新(化名)和其女友劉穎页响,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體段誊,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡闰蚕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了连舍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片没陡。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖索赏,靈堂內(nèi)的尸體忽然破棺而出诗鸭,到底是詐尸還是另有隱情,我是刑警寧澤参滴,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布强岸,位于F島的核電站,受9級(jí)特大地震影響砾赔,放射性物質(zhì)發(fā)生泄漏蝌箍。R本人自食惡果不足惜青灼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望妓盲。 院中可真熱鬧杂拨,春花似錦、人聲如沸悯衬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽筋粗。三九已至策橘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間娜亿,已是汗流浹背丽已。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留买决,地道東北人沛婴。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像督赤,于是被迫代替她去往敵國和親嘁灯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容

  • GitHub下載 廢話 在做表情鍵盤時(shí)躲舌,很多時(shí)候?yàn)榱耸沟酶鱾€(gè)平臺(tái)的表情得到統(tǒng)一(或者是對(duì)表情的擴(kuò)展等等)丑婿,因此采用...
    Chakery閱讀 10,020評(píng)論 5 17
  • 不知不覺,杏花已經(jīng)開了孽糖,擁擁簇簇枯冈,好不開心毅贮。有些含苞待放办悟,似乎在憋足勁兒,爭(zhēng)先恐后的展示自己滩褥,一片歡喜的模...
    625夭桃閱讀 125評(píng)論 0 1
  • 酒后是一種狀態(tài)不一定需要喝酒 讓照片喝酒一切充滿想像也是酒后
    北書房閱讀 993評(píng)論 10 12
  • 關(guān)于李子柒的本質(zhì): 昨天我在看她的視頻病蛉,本來努力地想分析點(diǎn)什么,視頻里的子柒有田地瑰煎,房屋铺然,生產(chǎn)工具,可以說是自耕農(nóng)...
    陸循生閱讀 928評(píng)論 0 3