美文网首页
IM项目中的自定义小表情实现

IM项目中的自定义小表情实现

作者: wzmyyj | 来源:发表于2020-05-10 15:02 被阅读0次

    前言

    在im项目(Android)中,用户发消息,喜欢在文字中嵌入一些小表情,以表达发送者当时的情感。除了系统输入法自带的emoji表情(emoji其实是特殊的文字)外。项目希望带一些更漂亮,带产品特色文化的自定义小表情(小图片)。

    图片嵌入在文字中显示,很明显可以使用ImageSpan去实现该效果。

    效果如图:

    效果图

    实现:

    实现上,主要问题是,实现文字与表情的转换。因此需要定义一套对应关系。

    这里采用类似微信的实现,[key]对应表情。比如: [微笑] 对应 😊。

    工具类:

    object EmoticonHelper {
    
        private const val SIGN_LEFT = '['
        private const val SIGN_RIGHT = ']'
        private const val ZOOM_SIZE = 1.3F
        private const val CACHE_SIZE = 60
    
        private val def = R.drawable.im_emoticon_def
        private val keyList = ArrayList<String>()
        private val cache = LruCache<String, Drawable>(CACHE_SIZE)
        // 表情。
        private val map = hashMapOf(
                "微笑" kto R.drawable.im_emoticon_wx,
                "撇嘴" kto R.drawable.im_emoticon_pz,
                "色" kto R.drawable.im_emoticon_se,
                "得意" kto R.drawable.im_emoticon_dy,
                "大哭" kto R.drawable.im_emoticon_dk,
                "发呆" kto R.drawable.im_emoticon_fd,
    
                "闭嘴" kto R.drawable.im_emoticon_bz,
                "睡" kto R.drawable.im_emoticon_shui,
                "流泪" kto R.drawable.im_emoticon_ll,
                "尴尬" kto R.drawable.im_emoticon_gg,
                "发怒" kto R.drawable.im_emoticon_fn,
                "调皮" kto R.drawable.im_emoticon_tb,
    
                "惊讶" kto R.drawable.im_emoticon_jy,
                "囧" kto R.drawable.im_emoticon_jiong,
                "吐" kto R.drawable.im_emoticon_tu,
                "哇" kto R.drawable.im_emoticon_wa,
                "偷笑" kto R.drawable.im_emoticon_tx,
                "愉快" kto R.drawable.im_emoticon_yk,
    
                "白眼" kto R.drawable.im_emoticon_by,
                "恐惧" kto R.drawable.im_emoticon_kj,
                "衰" kto R.drawable.im_emoticon_shuai,
                "笑哭" kto R.drawable.im_emoticon_kx,
                "无语" kto R.drawable.im_emoticon_ww,
                "晕" kto R.drawable.im_emoticon_yun,
    
                "困" kto R.drawable.im_emoticon_kun,
                "亲亲" kto R.drawable.im_emoticon_qq,
                "庆祝" kto R.drawable.im_emoticon_qz,
                "汗" kto R.drawable.im_emoticon_han,
                "咒骂" kto R.drawable.im_emoticon_zm,
                "嘘" kto R.drawable.im_emoticon_xu,
    
                "可怜" kto R.drawable.im_emoticon_kl,
                "失望" kto R.drawable.im_emoticon_sw,
                "憨笑" kto R.drawable.im_emoticon_hx,
                "呲牙" kto R.drawable.im_emoticon_cy,
                "拥抱" kto R.drawable.im_emoticon_yb,
                "思考" kto R.drawable.im_emoticon_sk,
    
                "口罩" kto R.drawable.im_emoticon_kz,
                "悠闲" kto R.drawable.im_emoticon_yxi,
                "委屈" kto R.drawable.im_emoticon_wq,
                "吐舌头" kto R.drawable.im_emoticon_tst,
                "鬼脸" kto R.drawable.im_emoticon_gl,
                "阴险" kto R.drawable.im_emoticon_yx,
    
                "啤酒" kto R.drawable.im_emoticon_pj,
                "玫瑰" kto R.drawable.im_emoticon_mg,
                "凋谢" kto R.drawable.im_emoticon_dx,
                "太阳" kto R.drawable.im_emoticon_ty,
                "火" kto R.drawable.im_emoticon_huo,
                "礼物" kto R.drawable.im_emoticon_lw,
    
                "爱心" kto R.drawable.im_emoticon_ax,
                "心碎" kto R.drawable.im_emoticon_xs,
                "强" kto R.drawable.im_emoticon_qiang,
                "弱" kto R.drawable.im_emoticon_ruo,
                "鼓掌" kto R.drawable.im_emoticon_gz,
                "OK" kto R.drawable.im_emoticon_ok,
    
                "蛋糕" kto R.drawable.im_emoticon_dg,
                "合十" kto R.drawable.im_emoticon_h10,
                "胜利" kto R.drawable.im_emoticon_sl,
                "握手" kto R.drawable.im_emoticon_ws,
                "红包" kto R.drawable.im_emoticon_hb,
                "钱" kto R.drawable.im_emoticon_qian
        )
    
        /**
         * 转换表情。
         */
        fun transEmoticon(context: Context, text: CharSequence, size: Float): Spannable {
            val ss = SpannableString.valueOf(text)!!
            spanEmoticon(context, ss, 0, ss.length, size)
            return ss
        }
    
        /**
         * span 表情。返回最后一个span的末尾位置(不包含)。
         */
        fun spanEmoticon(context: Context, sp: Spannable, startSp: Int, endSp: Int, size: Float): Int {
            if (endSp - startSp <= 2) return startSp
            var last = startSp
            val wh = size.toZoom()
            var start = sp.indexOf(SIGN_LEFT, startSp)
            while (start > -1) {
                val end = sp.indexOf(SIGN_RIGHT, start)
                if (end <= start || end >= endSp) break
                val key = sp.substring(start + 1, end)
                if (key in map.keys) {
                    val drawable = getDrawable(context, key, wh) ?: continue
                    sp.setSpan(ImageSpan(drawable), start, end + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
                    last = end + 1
                }
                start = sp.indexOf(SIGN_LEFT, start + 1)
            }
            return last
        }
        
        /**
         * 获取表情列表。
         */
        fun getEmoticonList(): List<Emoticon> {
            return keyList.map { Emoticon(it, it.toCode(), map[it] ?: def) }
        }
    
        class Emoticon(val key: String, val code: String, @DrawableRes val resId: Int)
    
        //---------private method-----------//
    
        /**
         * 获取 Drawable 并根据 key 和 大小 缓存。
         */
        private fun getDrawable(context: Context, key: String, size: Int): Drawable? {
            return cache[key + size] ?: ContextCompat.getDrawable(context, map[key] ?: def)?.apply {
                cache.put(key + size, this)
                this.setBounds(0, 0, size, size)
            }
        }
    
        /**
         * 转换成 code。
         */
        private fun String.toCode() = SIGN_LEFT + this + SIGN_RIGHT
    
        /**
         * 缩放大小。
         */
        private fun Float.toZoom() = (this * ZOOM_SIZE).toInt()
    
        /**
         * K-V 对,同时保存 key。
         */
        private infix fun String.kto(that: Int): Pair<String, Int> {
            keyList.add(this)
            return Pair(this, that)
        }
    
    }
    

    主要就是做一个转换功能。同时需要考虑一下性能优化,否则效率低就会卡顿。

    PS:这里优化了 查询转换策略 和 Drawable复用策略,供参考。

    :Spannable有关的操作,少用String。使用CharSequence,因为不一定是String。用SpannableString.valueOf(text) 代替new SpannableString(text)

    使用:

    在TextView上使用,也写个BindingAdapter方法。

    @BindingAdapter(value = ["binding_text_emoticon"], requireAll = true)
    fun TextView.setEmoticonText(text: CharSequence?) {
        if (this.text?.toString() != text) {
            this.text = if (text != null) {
                EmoticonHelper.transEmoticon(context, text, textSize)
            } else {
                ""
            }
        }
    }
    
    @BindingAdapter(value = ["binding_text_emoticon", "binding_text_emoticon_ellipsize"], requireAll = true)
    fun TextView.setEmoticonText(text: CharSequence?, avail: Float) {
        if (this.text?.toString() != text) {
            this.text = if (text != null) {
                val emo = EmoticonHelper.transEmoticon(context, text, textSize)
                TextUtils.ellipsize(emo, paint, avail, TextUtils.TruncateAt.END)
            } else {
                ""
            }
        }
    }
    

    注:其中TextUtils.ellipsize(emo, paint, avail, TextUtils.TruncateAt.END) 是为了解决表情在单行textView显示不下时显“...”.的问题。直接默认用TextView的ellipsize属性,对表情(ImageSpan)无效,会截成半个。

    输入框:

    表情要在输入框中显示。根据输入code,自动转换成表情(ImageSpan)。

    方案1:给EditView设置监听,在文字变化后将文字做个转换。这样效率超低,输入越多越卡。否决!

    方案2:根据具体变化的文本设置转换。

    editText.addTextChangedListener(object : TextWatcher {
                override fun afterTextChanged(s: Editable?) {
                    
                }
    
                override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
    
                }
    
                override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                    if (s !is Spannable) return
                    // 输入会能影响到的包含前后几格。
                    val end = start + count
                    val sl = s.lastIndexOf('[', start)
                    val st = if (sl > -1 && start <= s.indexOf(']', sl)) {
                        sl
                    } else {
                        start
                    }
                    val er = s.indexOf(']', end)
                    val en = if (er > -1 && s.lastIndexOf('[', er) in 0 until end) {
                        er + 1
                    } else {
                        end
                    }
                    val last = EmoticonHelper.spanEmoticon(editText.context, s, st, en, editText.textSize)
                    // 如果输入影响后几格,即连同后几格一起变成表情。将光标置于表情末尾。
                    if (last > end && last <= s.length) {
                        Selection.setSelection(s, last)
                    }
                }
            })
    

    :当前输入的东西(可能是复制过来的多个字符)。可能会影响到前面或后面的几个字符。

    例如:原本文本:“[微]” ,在“微”后面输入一个“笑”,实际文本是“[微笑]”满足code。就会自动转变成😊表情。
    此时,光标在“笑”后面,需要代码控制把光标挪到“]”的后面。才符合实际输入效果。

    表情选择框操作

    删除:模拟退格,表情需要整个整个删。

    editText.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
    

    插入:将code插入到光标末尾。

    editText.run { text.insert(selectionEnd, code) }
    

    其他:

    转发到微信,有些表情微信里没有对应。转换成emoji代替。

        // 转发微信需要替换成 emoji 的表情。
        private val emojiMap = hashMapOf(
                "恐惧" to "\uD83D\uDE31",
                "笑哭" to "\uD83D\uDE02",
                "无语" to "\uD83D\uDE12",
                "庆祝" to "\uD83C\uDF89",
                "失望" to "\uD83D\uDE14",
                "思考" to "\uD83E\uDD14",
                "口罩" to "\uD83D\uDE37",
                "吐舌头" to "\uD83D\uDE1D",
                "鬼脸" to "\uD83D\uDC7B",
                "火" to "\uD83D\uDD25",
                "合十" to "\uD83D\uDE4F",
                "钱" to "\uD83D\uDCB0",
                "礼物" to "\uD83C\uDF81"
        )
        
         /**
         * 转发微信。不支持的 code 转化为 emoji 。
         */
        fun transCodeToEmoji(text: String): String {
            var str = text
            for (key in emojiMap.keys) {
                val code = key.toCode()
                if (str.contains(code)) {
                    str = str.replace(code, emojiMap[key].orEmpty())
                }
            }
            return str
        }
    

    总结:

    要点:

    1. ImageSpan实现表情的显示。😊
    2. code与Drawable的对应关系。
    3. Drawable性能的考量。
    4. 表情在EditText里输入的几个优化点。
    5. 微信转发时替换code。

    相关文章

      网友评论

          本文标题:IM项目中的自定义小表情实现

          本文链接:https://www.haomeiwen.com/subject/xajjnhtx.html