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

剖析ios微信谈天页面

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

各状况剖析

  1. 语音状况

语音状况时,语音与edit图标切换,EditText 与按住说话UI切换,此刻假如键盘处于修改状况,则收回键盘,此刻键盘处于表情面板或许更多面板需求收回面板,若表情面板时,表情与edit图方位康复表情icon。

  1. 键盘状况

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

  1. 表情状况

注意语音与edit图标 方位康复即可

  1. 更多面板

注意语音与edit图标,表情与edit图标方位康复

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

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

键盘UI组成

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

所以能够将结构设置为:

<LinearLayout
    android:id="@+id/cl_voiceRoom_inputRoot"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:orientation="vertical"
    tools:visibility="visible">
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/imEditBgCL"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/im_chat_bottom_bg"
        android:minHeight="60dp">
        // 键盘顶部,表情输入框等
    </androidx.constraintlayout.widget.ConstraintLayout>
    // 指定面板占位
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/imMiddlewareVP"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:background="#F0EFEF"
        android:visibility="gone"
        app:layout_constraintTop_toBottomOf="@id/imEditBgCL"
        tools:visibility="visible"
        />
</LinearLayout>

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

键盘逻辑处理

  1. 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
}
  1. 依照上面的状况互斥,咱们需求动态监听软键盘的翻开和封闭

系统没有供给对应的完结,所以咱们才采纳的办法是,监听软键盘的高度改变

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时

    • 躲藏面板
    • 设置获取焦点
    • 翻开键盘 其他场景下切换没什么问题,可是当键盘和自定义面板切换时有或许呈现这样的问题:

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

由于键盘的封闭和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 表情能够经过以下方式在字符串中表明:

  1. Unicode 编码:Emoji 表情的 Unicode 编码能够直接嵌入到字符串中,例如 "\u2764\ufe0f" 表明一个赤色的心形 Emoji。其间,\u 是 Unicode 转义字符,后边跟着 4 个十六进制数表明该字符的 Unicode 编码。
  2. Unicode 代码点:Unicode 代码点是 Unicode 编码的十进制表明,能够运用 &# 后跟代码点数字和分号 ; 来表明 Emoji,例如 😀 表明一个笑脸 Emoji。在 XML 中,能够运用 &#x 后跟代码点的十六进制表明来表明 Emoji,例如 😀 表明一个笑脸 Emoji。
  3. Emoji 表情符号:在 Android 4.4 及以上版别中,能够直接运用 Emoji 表情符号来表明 Emoji,例如 表明一个浅笑的 Emoji。在 Android 4.3 及以下版别中,需求运用第一种或第二种方式来表明 Emoji。

我在此demo中运用第一种完结的,详细运用过程:

  1. UI布局
  2. 数据
  • 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 服务运用可下载字体

  1. 在Layout Editor中,挑选一个 TextView,然后在Properties下,挑选fontFamily > More Fonts。

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

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

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

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

<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
        app:fontProviderAuthority="com.google.android.gms.fonts"
        app:fontProviderPackage="com.google.android.gms"
        app:fontProviderQuery="Montserrat Subrayada"
        app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>

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 <= text.length) {
        val codePoint = text.codePointBefore(index)
        deleteLength = if (Character.isSupplementaryCodePoint(codePoint)) 2 else 1
    }
    inputConnection.deleteSurroundingText(deleteLength, 0)
}
  1. 首要,经过 editText.onCreateInputConnection(EditorInfo()) 办法获取输入连接器(InputConnection),它能够用于向 EditText 发送文本和操控命令。在这里,咱们运用它来删去文本。
  2. 接着,获取 EditText 中当时的文本,并找到要删去的字符的鸿沟。经过 editText.selectionStart办法获取当时文本的光标方位,然后运用 text.codePointBefore(index)办法获取光标方位前面一个字符的 Unicode 编码点。假如该字符是一个 Unicode 表情符号,它或许由多个 Unicode 编码点组成,因而需求运用 Character.isSupplementaryCodePoint(codePoint) 办法来判别该字符是否需求删去多个编码点。
  3. 最终,运用 inputConnection.deleteSurroundingText(deleteLength, 0)办法删去要删去的字符。其间,deleteLength 是要删去的字符数,0 表明没有要插入的新文本。

主要的技能点在于“text.codePointBefore(index)办法获取光标方位前面一个字符的 Unicode 编码点,然后向前勘探,找到字符鸿沟” 以此完结删去操作

翻开面板时 RV布局的处理

这个就比较简略了

  1. 首要,经过 root.viewTreeObserver.addOnGlobalLayoutListener 办法添加一个全局布局监听器,该监听器能够监听整个布局树的改变,包含软键盘的弹出和躲藏。
  2. 在监听器的回调函数中,经过 root.getWindowVisibleDisplayFrame(r) 办法获取当时窗口的可见区域(不包含软键盘),并经过 root.rootView.height 办法获取整个布局树的高度,从而计算出软键盘的高度 keypadHeight。
  3. 接着,经过计算屏幕高度的 15% 来判别软键盘是否弹出。假如软键盘高度超过了屏幕高度的 15%,则认为软键盘现已弹出。
  4. 假如软键盘现已弹出,则经过 imMiddlewareRV.scrollToPosition(mAdapter.getItemCount() – 1) 办法将 RecyclerView滚动到最终一条音讯的方位,以保证用户一直能看到最新的音讯
root.viewTreeObserver.addOnGlobalLayoutListener {
    val r = Rect()
    root.getWindowVisibleDisplayFrame(r)
    val screenHeight = root.rootView.height
    val keypadHeight = screenHeight - r.bottom
    //键盘是否弹出
    val diff = screenHeight * 0.15
    if (keypadHeight > diff) { // 15% of the screen height
        imMiddlewareRV.scrollToPosition(mAdapter.getItemCount() - 1);
    }
}

总结

仿照微信谈天键盘的办法,完结了一个包含表情等功能的键盘区域,并处理了一些常见的问题。经过实践和查询,处理了切换键盘的问题,并完结了Emoji的Unicode显现和自定义删去时向前探究字符鸿沟完结表情删去等操作。在过程中,认为很简略的一个东西花了大量的时间查询原因,发现键盘这一块水很深,当我看到ResultReceiver时,看到了AIDL通讯,所以再Android这个系统中,Binder的机制需求了然于胸的,刚好我最近在学习Binder得各种知识,不久后会发布对应的博客,关注我,哈哈。

此系列属于我的一个 《Android IM即时通讯多进程中间件设计与完结》 系列的一部分,能够看看这个系列

项目地址