Android平台无需第三方库实现WhatsApp风格表情布局问询
嘿,这个需求完全可以用Android原生组件搞定,不用依赖任何第三方库!我帮不少开发者实现过类似WhatsApp的表情布局,咱们一步步来拆解实现:
一、先准备表情的数据结构和资源
首先得把表情的信息结构化,方便后续分类和展示。我们可以定义一个数据类存储表情的核心信息,同时按分类区分(比如默认、开心、爱心这些):
// 表情数据类 data class Emoji( val resId: Int, // 本地表情资源ID val unicode: String, // 对应Unicode字符串,用于插入输入框 val category: EmojiCategory // 所属分类 ) // 表情分类枚举 enum class EmojiCategory { DEFAULT, LAUGH, LOVE, ANGRY, SAD // 可根据需求扩展更多分类 }
接下来把你准备好的表情资源(比如drawable里的表情图片)按分类整理成列表,比如:
private fun getEmojisByCategory(category: EmojiCategory): List<Emoji> { return when(category) { EmojiCategory.DEFAULT -> listOf( Emoji(R.drawable.emoji_smile, "😀", EmojiCategory.DEFAULT), Emoji(R.drawable.emoji_laugh, "😂", EmojiCategory.DEFAULT), // 更多默认表情... ) EmojiCategory.LOVE -> listOf( Emoji(R.drawable.emoji_heart, "❤️", EmojiCategory.LOVE), // 更多爱心类表情... ) // 其他分类的表情列表... } }
二、构建表情面板的整体布局
WhatsApp的表情面板是底部弹出的,包含分类标签栏、分页表情区域和页面指示器。我们用AndroidX的BottomSheetBehavior实现底部弹出效果,搭配ViewPager2做分页,RecyclerView做网格展示:
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 主内容区域:聊天记录+输入框 --> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/chat_recycler" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> <!-- 输入操作栏 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="8dp"> <ImageView android:id="@+id/emoji_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_emoji"/> <EditText android:id="@+id/input_edit_text" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="输入消息"/> <Button android:id="@+id/send_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="发送"/> </LinearLayout> </LinearLayout> <!-- 表情面板(底部弹出) --> <LinearLayout android:id="@+id/emoji_panel" android:layout_width="match_parent" android:layout_height="250dp" android:orientation="vertical" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> <!-- 分类标签栏 --> <HorizontalScrollView android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="none"> <LinearLayout android:id="@+id/emoji_category_container" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="8dp"/> </HorizontalScrollView> <!-- 表情分页区域 --> <androidx.viewpager2.widget.ViewPager2 android:id="@+id/emoji_view_pager" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> <!-- 页面指示器 --> <LinearLayout android:id="@+id/indicator_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center" android:padding="4dp"/> </LinearLayout> </LinearLayout>
三、实现分类标签的切换逻辑
分类标签需要动态生成,点击时切换对应的表情页面,同时更新选中状态(比如文字变色、下划线):
private fun initCategoryTabs() { val categories = EmojiCategory.values() categories.forEach { category -> val tab = TextView(this) tab.text = when(category) { EmojiCategory.DEFAULT -> "默认" EmojiCategory.LOVE -> "爱心" // 给分类设置友好名称 else -> category.name } tab.textSize = 14f tab.setPadding(16, 8, 16, 8) tab.setOnClickListener { val targetPosition = categories.indexOf(category) emoji_view_pager.currentItem = targetPosition updateTabSelectedState(tab) updateIndicatorState(targetPosition) } emoji_category_container.addView(tab) } // 默认选中第一个标签 updateTabSelectedState(emoji_category_container.getChildAt(0) as TextView) } // 更新标签选中状态 private fun updateTabSelectedState(selectedTab: TextView) { for (i in 0 until emoji_category_container.childCount) { val tab = emoji_category_container.getChildAt(i) as TextView tab.setTextColor( if (tab == selectedTab) ContextCompat.getColor(this, R.color.blue) else ContextCompat.getColor(this, R.color.gray) ) tab.paint.isUnderlineText = tab == selectedTab } }
四、实现表情网格的分页展示
每个分类对应一个Fragment,里面用RecyclerView展示网格表情。我们用FragmentStateAdapter给ViewPager2提供页面:
1. 表情页面Fragment
class EmojiPageFragment : Fragment() { private lateinit var emojiList: List<Emoji> private var onEmojiClick: ((Emoji) -> Unit)? = null companion object { private const val ARG_EMOJIS = "emojis" fun newInstance(emojis: List<Emoji>) = EmojiPageFragment().apply { arguments = Bundle().apply { putParcelableArrayList(ARG_EMOJIS, ArrayList(emojis)) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) emojiList = arguments?.getParcelableArrayList(ARG_EMOJIS) ?: emptyList() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_emoji_page, container, false) val recyclerView = view.findViewById<RecyclerView>(R.id.emoji_recycler) // 设置网格布局,每行显示8个表情(和WhatsApp一致) recyclerView.layoutManager = GridLayoutManager(context, 8) recyclerView.adapter = EmojiAdapter(emojiList) { emoji -> onEmojiClick?.invoke(emoji) } return view } fun setOnEmojiClickListener(listener: (Emoji) -> Unit) { onEmojiClick = listener } }
对应的fragment_emoji_page.xml就是一个简单的RecyclerView:
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/emoji_recycler" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="8dp"/>
2. 表情列表Adapter
class EmojiAdapter( private val emojiList: List<Emoji>, private val onItemClick: (Emoji) -> Unit ) : RecyclerView.Adapter<EmojiAdapter.EmojiViewHolder>() { inner class EmojiViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val emojiIv: ImageView = itemView.findViewById(R.id.emoji_image) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_emoji, parent, false) return EmojiViewHolder(view) } override fun onBindViewHolder(holder: EmojiViewHolder, position: Int) { val emoji = emojiList[position] holder.emojiIv.setImageResource(emoji.resId) holder.itemView.setOnClickListener { onItemClick(emoji) } } override fun getItemCount() = emojiList.size }
对应的item_emoji.xml:
<ImageView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/emoji_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="4dp" android:scaleType="centerInside"/>
3. 关联ViewPager2和Adapter
在主Activity中设置ViewPager2的适配器,并监听页面切换:
private fun initEmojiPager() { val categories = EmojiCategory.values() emoji_view_pager.adapter = object : FragmentStateAdapter(this) { override fun getItemCount() = categories.size override fun createFragment(position: Int): Fragment { val category = categories[position] val emojis = getEmojisByCategory(category) return EmojiPageFragment.newInstance(emojis).apply { setOnEmojiClickListener { emoji -> insertEmojiIntoInput(emoji) } } } } // 页面切换时同步更新标签和指示器 emoji_view_pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { super.onPageSelected(position) val selectedTab = emoji_category_container.getChildAt(position) as TextView updateTabSelectedState(selectedTab) updateIndicatorState(position) } }) }
五、处理表情点击插入输入框的逻辑
当用户点击表情时,把对应的Unicode字符插入到输入框的光标位置:
private fun insertEmojiIntoInput(emoji: Emoji) { val inputEt = findViewById<EditText>(R.id.input_edit_text) val cursorPos = inputEt.selectionStart val originalText = inputEt.text.toString() // 拼接新文本 val newText = originalText.substring(0, cursorPos) + emoji.unicode + originalText.substring(cursorPos) inputEt.setText(newText) // 光标移动到插入后的位置 inputEt.setSelection(cursorPos + emoji.unicode.length) }
六、添加页面指示器
为了更贴近WhatsApp的体验,我们给ViewPager2添加底部圆点指示器:
private fun initIndicator() { val categories = EmojiCategory.values() categories.forEach { val dot = View(this) val params = LinearLayout.LayoutParams(8, 8) params.setMargins(4, 0, 4, 0) dot.layoutParams = params dot.setBackgroundResource(R.drawable.indicator_dot_unselected) indicator_container.addView(dot) } // 默认选中第一个圆点 updateIndicatorState(0) } // 更新指示器选中状态 private fun updateIndicatorState(selectedPosition: Int) { for (i in 0 until indicator_container.childCount) { val dot = indicator_container.getChildAt(i) dot.setBackgroundResource( if (i == selectedPosition) R.drawable.indicator_dot_selected else R.drawable.indicator_dot_unselected ) } }
需要两个drawable文件:
indicator_dot_selected.xml(选中状态):
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="@color/blue"/> </shape>
indicator_dot_unselected.xml(未选中状态):
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="@color/light_gray"/> </shape>
七、优化交互体验
最后加上表情面板的显示隐藏逻辑,点击表情按钮切换状态,点击输入框自动隐藏面板:
private fun initBottomSheet() { val bottomSheetBehavior = BottomSheetBehavior.from(emoji_panel) // 默认隐藏面板 bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN // 表情按钮切换面板显示 emoji_btn.setOnClickListener { bottomSheetBehavior.state = if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_HIDDEN) { BottomSheetBehavior.STATE_EXPANDED } else { BottomSheetBehavior.STATE_HIDDEN } } // 点击输入框隐藏面板 input_edit_text.setOnClickListener { bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN } }
这样一套实现下来,完全用Android原生组件完成,没有任何第三方库依赖,效果和WhatsApp的表情布局基本一致。你可以根据自己的需求调整分类数量、每行表情个数、样式配色这些细节~
内容的提问来源于stack exchange,提问作者Tim

