实战经验:打造仿微信聊天键盘,解决常见问题 您所在的位置:网站首页 emoji表情更新安卓OPPO 实战经验:打造仿微信聊天键盘,解决常见问题

实战经验:打造仿微信聊天键盘,解决常见问题

2023-11-19 09:19| 来源: 网络整理| 查看: 265

防苹果微信聊天页面,聊天中的布局不是,主要是键盘部分,键盘部分在做的过程中遇到了几个坑,记录一下,看看大家有没有越到过

output_image.gif

分析ios微信聊天页面

UI组成看起来比较简单,但是包含的内容可真不少,首先语音、输入框、表情、更多四个简单元素,元素间存在互斥的一些状态操作,比如语音时,显示按住说话,键盘关闭,表情面板时面板关闭,面板关闭则联动表情和EditText图标的切换。

各状态分析 语音状态

语音状态时,语音与edit图标切换,EditText 与按住说话UI切换,此时如果键盘处于编辑状态,则收回键盘,此时键盘处于表情面板或者更多面板需要收回面板,若表情面板时,表情与edit图位置恢复表情icon。

键盘状态

点击语音与edit图标 位置时,icon 为语音标,键盘弹出,当前再表情面板时,点击表情与edit图标, 键盘弹出,icon 变换

表情状态

注意语音与edit图标 位置恢复即可

更多面板

注意语音与edit图标,表情与edit图标位置恢复

对于这四种状态直接使用LiveData, 然后与点击事件做出绑定,事件发生时处理对应状态即可

image.png

键盘UI组成

image.png

所以可以将结构设置为:

// 键盘顶部,表情输入框等 // 指定面板占位

然后对应上述的状态进行UI和键盘的操作

键盘逻辑处理 EditText 自动换行输入并将action设置为send 按钮

这一步很简单,但是有一个坑,按照正常逻辑,再xml中的EditText 设置以下属性,即可完成这个需求

android:imeOptions="actionSend" android:inputType="textMultiLine"

按照属性的原义,这样将显示正常的发送按钮以及可自动多行输入,但是就是不显示发送,查资料发现imeOptions 需要使inputType 为text 时才显示,但是又实现不了我们的需求,最后处理方式

android:imeOptions="actionSend" android:inputType="text" //然后在代码中进行如下设置: binding.imMiddlewareET.run { imeOptions = EditorInfo.IME_ACTION_SEND setHorizontallyScrolling(false) maxLines = Int.MAX_VALUE } 按照上面的状态互斥,我们需要动态监听软键盘的打开和关闭

系统没有提供对应的实现,所以我们才采取的办法是,监听软键盘的高度变化

View rootView = getWindow().getDecorView().getRootView(); rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { Rect rect = new Rect(); rootView.getWindowVisibleDisplayFrame(rect); int heightDiff = rootView.getHeight() - rect.bottom; boolean isSoftKeyboardOpened = heightDiff > 0; // 处理软键盘打开或关闭的逻辑 } });

通过判断高度来推算键盘的打开或者关闭

解决切换键盘问题

切换键盘时,比如表情和Edit 切换

当面板是键盘时,点击图标区域

取消Edit焦点 关闭键盘 打开emoji面板

当面板是emoji时

隐藏面板 设置获取焦点 打开键盘 其他场景下切换没什么问题,但是当键盘和自定义面板切换时有可能出现这样的问题:

image.png

因为键盘的关闭和View的显示,或者View的隐藏和键盘的显示那个先执行完毕逻辑不能串行,导致会出现这种闪烁的画面

解决方案:

分析上述问题后会发现,导致的出现这种情况的原因就是逻辑不能串行,那我们保证二者的逻辑串行就不会出现这问题了,怎么保证呢?

首先要知道的是肯定不能让View先行,View先行一样会出现这个问题,所以要保证让键盘先行,我们看一下,键盘的打开和关闭:

// 显示键盘 private fun showSoftKeyBoard(view: View) { val imm = mContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? imm?.showSoftInput(view, InputMethodManager.SHOW_FORCED) } // 隐藏键盘 private fun hideSoftKeyBoard(view: View) { val imm = mContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? if (imm != null && imm.isActive) { imm.hideSoftInputFromWindow(view.windowToken, 0) } }

这个代码对于键盘的显示隐藏是没有任何问题的,但是我们怎么判断它执行这个动作完毕了呢?

方法一:

上面我们有这样的操作,监听了键盘高度的监听,我们可以在执行切换操作时启动一个线程的死循环,然后再循环中判断高度,满足高度时执行上述逻辑。

方法二:

看下InputMethodManager 的源码,发现:

/** * Synonym for {@link #hideSoftInputFromWindow(IBinder, int, **ResultReceiver**)} * without a result: request to hide the soft input window from the * context of the window that is currently accepting input. * * @param windowToken The token of the window that is making the request, * as returned by {@link View#getWindowToken() View.getWindowToken()}. * @param flags Provides additional operating flags. Currently may be * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set. */ public boolean hideSoftInputFromWindow(IBinder windowToken, int flags) { return hideSoftInputFromWindow(windowToken, flags, null); }

是不是很神奇,这个隐藏方法有一个ResultReceiver 的回调,卧槽,是不是看这个名字就感觉有戏,具体看一下:

public boolean hideSoftInputFromWindow(IBinder windowToken, int flags, ResultReceiver resultReceiver) { return hideSoftInputFromWindow(windowToken, flags, resultReceiver, SoftInputShowHideReason.HIDE_SOFT_INPUT); }

ResultReceiver 是一个用于在异步操作完成时接收结果的类,它可以让你在不同的线程之间进行通信。在 hideSoftInputFromWindow() 方法中,ResultReceiver 作为一个可选参数,用于指定当软键盘隐藏完成时的回调。该回调会在后台线程上执行,因此不会阻塞主线程,从而提高应用程序的响应性能。

ResultReceiver 类有一个 onReceiveResult(int resultCode, Bundle resultData) 方法,当异步操作完成时,该方法会被调用。通过实现该方法,你可以自定义处理异步操作完成后的行为。例如,在软键盘隐藏完成后,你可能需要执行一些操作,例如更新 UI 或者执行其他任务。

在 hideSoftInputFromWindow()方法中,你可以通过传递一个 ResultReceiver 对象来指定异步操作完成后的回调。当软键盘隐藏完成时,系统会调用ResultReceiver对象的send()方法,并将结果代码和数据包装在 Bundle对象中传递给 ResultReceiver对象。然后,ResultReceiver 对象的 onReceiveResult() 方法会在后台线程上执行,以便你可以在该方法中处理结果。

然后看了showSoftInput 也同样有这个参数

public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) { return showSoftInput(view, flags, resultReceiver, SoftInputShowHideReason.SHOW_SOFT_INPUT); }

那我们可以这样解决:

隐藏为例: 当我执行切换时,首先调用hideSoftInputFromWindow, 并创建ResultReceiver监听,当返回结果后,执行View的操作,保证他们的串行,以此解决切换键盘闪烁问题。

private fun hideSoftKeyBoard(view: View, callback: () -> Unit) { val imm = mActivity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? if (imm != null && imm.isActive) { val resultReceiver = object : ResultReceiver(Handler(Looper.getMainLooper())) { override fun onReceiveResult(resultCode: Int, resultData: Bundle?) { super.onReceiveResult(resultCode, resultData) // 在这里处理软键盘隐藏完成后的逻辑 callback.invoke() //... } } imm.hideSoftInputFromWindow(view.windowToken, 0, resultReceiver) } } Emoji 显示

在 Android 中,Emoji 表情可以通过以下方式在字符串中表示:

Unicode 编码:Emoji 表情的 Unicode 编码可以直接嵌入到字符串中,例如 "\u2764\ufe0f" 表示一个红色的心形 Emoji。其中,\u 是 Unicode 转义字符,后面跟着 4 个十六进制数表示该字符的 Unicode 编码。 Unicode 代码点:Unicode 代码点是 Unicode 编码的十进制表示,可以使用 &# 后跟代码点数字和分号 ; 来表示 Emoji,例如 😀 表示一个笑脸 Emoji。在 XML 中,可以使用 &#x 后跟代码点的十六进制表示来表示 Emoji,例如 😀 表示一个笑脸 Emoji。 Emoji 表情符号:在 Android 4.4 及以上版本中,可以直接使用 Emoji 表情符号来表示 Emoji,例如 😊 表示一个微笑的 Emoji。在 Android 4.3 及以下版本中,需要使用第一种或第二种方式来表示 Emoji。

我在此demo中使用第一种实现的,具体使用步骤:

UI布局 数据 unicode.org/Public/emoj… 下载表情内容 解析表情数据, 多个十六进制的我没写, flow { val pattern = Regex("^(\S+)\s+;\s+fully-qualified\s+#\s+((?:\S+\s+)+)(.+)$") val filterNotNull = readAssetsFile("emoji.txt", IMApplication.context) .trim() .lines() .map { line -> val matchResult = pattern.find(line) if (matchResult != null) { val (emoji, codePointHex, comment) = matchResult.destructured val codePoint = emoji.drop(2).toInt(16) EmojiEntry(emoji, codePoint, "E${emoji.take(2)}", comment,codePointHex) } else { null } }.filterNotNull() emit(filterNotNull) } 使用 使用google 提供的emoji库 implementation 'androidx.emoji:emoji:1.1.0' 在Application中初始化 val fontRequest = FontRequest( "com.google.android.gms.fonts", "com.google.android.gms", "Montserrat Subrayada", R.array.com_google_android_gms_fonts_certs ) val config = FontRequestEmojiCompatConfig(this, fontRequest) EmojiCompat.init(config)

对于FontRequest 是使用的Goolge 提供的可下载字体配置进行初始化的,当然可以不用,但是系统的字体对于表情不是高亮的,看起来是灰色的(也可以给TextView 设置字体解决)

通过 Android Studio 和 Google Play 服务使用可下载字体 在 Layout Editor 中,选择一个 TextView,然后在 Properties 下,选择 fontFamily > More Fonts。

image.png

在 Source 下拉列表中,选择 Google Fonts。 在 Fonts 框中,选择一种字体。 选择 Create downloadable font,然后点击 OK

image.png

然后会在项目的res 下生成文字

Emoji 面板中的删除操作

再IOS微信中,点击Emoji面板后输入框是没有焦点的,然后点击删除时Emoji会有一个问题,因为它的大小是2个byte,所以常规删除是不行的,

expressionDeleteFL.setOnClickListener { val inputConnection = editText.onCreateInputConnection( EditorInfo() ) // 找到要删除的字符的边界 val text = editText.text.toString() val index = editText.selectionStart var deleteLength = 1 if (index > 0 && index diff) { // 15% of the screen height imMiddlewareRV.scrollToPosition(mAdapter.getItemCount() - 1); } } 总结

仿照微信聊天键盘的方法,实现了一个包含表情等功能的键盘区域,并解决了一些常见的问题。通过实践和调查,解决了切换键盘的问题,并实现了Emoji的Unicode显示和自定义删除时向前探索字符边界完成表情删除等操作。在过程中,以为很简单的一个东西花了大量的时间调查原因,发现键盘这一块水很深,当我看到ResultReceiver时,看到了AIDL通信,所以再Android这个体系中,Binder的机制需要了然于胸的,刚好我最近在学习Binder得各种知识,不久后会发布对应的博客,关注我,哈哈。

此系列属于我的一个 《Android IM即时通信多进程中间件设计与实现》 系列的一部分,可以看看这个系列

项目地址



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有