You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Compose聊天应用添加表情面板的实现问题

在Android Compose聊天应用中集成表情面板

1. 依赖配置

首先在模块级build.gradle(或build.gradle.kts)中添加vanniktech.emoji库依赖:

// 替换为最新稳定版本
implementation "com.vanniktech:emoji-google:0.16.0"

2. 改造输入框:嵌入原生EditText

由于vanniktech.emoji依赖View系统的EditText,我们需要在Compose中用AndroidView包裹原生EditText,同时对齐原Compose TextField的样式:

@Composable
fun ChatEditText(
    text: String,
    onValueChange: (String) -> Unit,
    onEmojiIconClick: () -> Unit
) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.width(248.dp)
    ) {
        AndroidView(
            factory = { context ->
                EditText(context).apply {
                    tag = "chat_edit_text"
                    // 匹配原Compose TextField样式
                    setBackgroundColor(Color(0xFFF2F2F2).toArgb())
                    hint = "Type a message"
                    setHintTextColor(Color.Gray.toArgb())
                    maxLines = 10
                    // 设置圆角背景(可通过drawable或代码实现)
                    background = GradientDrawable().apply {
                        shape = GradientDrawable.RECTANGLE
                        cornerRadius = 40.dp.toPx(context).toFloat()
                        setColor(Color(0xFFF2F2F2).toArgb())
                    }
                    // 同步输入内容到Compose状态
                    addTextChangedListener(object : TextWatcher {
                        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
                        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
                        override fun afterTextChanged(s: Editable?) {
                            s?.toString()?.let(onValueChange)
                        }
                    })
                }
            },
            update = { editText ->
                if (editText.text.toString() != text) {
                    editText.setText(text)
                }
            },
            modifier = Modifier.weight(1f)
        )

        // 表情切换图标
        Icon(
            painter = painterResource(id = R.drawable.emoji),
            contentDescription = "Insert emoticon",
            tint = Color(0xff808080),
            modifier = Modifier
                .padding(start = 8.dp)
                .clickable { onEmojiIconClick() }
        )
    }
}

// 扩展函数:dp转px
fun Dp.toPx(context: Context): Float = this.value * context.resources.displayMetrics.density

3. 创建并管理EmojiPopup

在ChatScreen中,通过LocalView.current获取根View,创建EmojiPopup实例并处理面板显示/隐藏、表情插入逻辑:

@Composable
fun ChatScreen() {
    var messageText by remember { mutableStateOf("") }
    val context = LocalContext.current
    val rootView = LocalView.current
    var emojiPanelHeight by remember { mutableStateOf(0.dp) }

    val emojiPopup = remember {
        EmojiPopup.Builder.fromRootView(rootView)
            .setOnEmojiClickListener { emoji ->
                val editText = rootView.findViewWithTag<EditText>("chat_edit_text")
                editText?.let { et ->
                    val cursorPos = et.selectionStart
                    et.text.insert(cursorPos, emoji.unicode)
                    et.setSelection(cursorPos + emoji.unicode.length)
                }
            }
            .setOnEmojiPopupShownListener { height ->
                emojiPanelHeight = height.dp
            }
            .setOnEmojiPopupDismissListener {
                emojiPanelHeight = 0.dp
                // 面板关闭后恢复软键盘(可选)
                val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
                rootView.findViewWithTag<EditText>("chat_edit_text")?.let {
                    imm.showSoftInput(it, 0)
                }
            }
            .build()
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Bottom
    ) {
        // 聊天消息列表
        LazyColumn(
            modifier = Modifier.weight(1f),
            contentPadding = PaddingValues(
                top = 16.dp,
                start = 16.dp,
                end = 16.dp,
                bottom = 16.dp + emojiPanelHeight
            )
        ) {
            // 替换为你的消息列表逻辑
            items(emptyList<ChatMessage>()) { message ->
                if (message.isSentByMe) {
                    SentMessageItem(message)
                } else {
                    ReceivedMessageItem(message)
                }
            }
        }

        // 输入区域
        Row(
            modifier = Modifier.padding(8.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            ChatEditText(
                text = messageText,
                onValueChange = { messageText = it },
                onEmojiIconClick = {
                    val editText = rootView.findViewWithTag<EditText>("chat_edit_text")
                    editText?.let { et ->
                        val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
                        if (emojiPopup.isShowing) {
                            emojiPopup.dismiss()
                            imm.showSoftInput(et, 0)
                        } else {
                            imm.hideSoftInputFromWindow(et.windowToken, 0)
                            emojiPopup.show()
                        }
                    }
                }
            )
        }
    }
}

// 示例消息数据类
data class ChatMessage(val content: String, val isSentByMe: Boolean)
@Composable fun SentMessageItem(message: ChatMessage) {}
@Composable fun ReceivedMessageItem(message: ChatMessage) {}

4. 核心逻辑说明

  • 通过AndroidView桥接原生EditText,解决Compose TextField无法直接适配View系表情库的问题
  • 利用tag快速定位EditText实例,实现表情插入和软键盘切换
  • 监听EmojiPopup的显示高度,动态调整LazyColumn的底部padding,避免消息列表被面板遮挡

内容的提问来源于stack exchange,提问作者Olaleye Paul Tobi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 02:23:32