iOS自定義表情鍵盤

GitHub下載

廢話

在做表情鍵盤時(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è)表情包含的不僅僅是圖片),表情大概具有以下屬性:

  1. ID (對應(yīng)的表情包id)
  2. image (對應(yīng)的圖片)
  3. code (十六進(jìn)制編碼, 如:系統(tǒng)表情)
  4. 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)致的,那么解決這樣的問題也就簡單了,只需要重寫 UICollectionViewLayoutUICollectionViewCell 重新布局便可。

代碼如下:

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è)人感覺是字符串,不懂這樣理解對否),因此不需要我們做太多的操作,可以直接給UITextViewtext屬性賦值。
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?方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容