這或許就是你想要的聊天鍵盤處理方案

歡迎轉載,轉載請註明出處:juejin.cn/post/684490…android

寫在前面

老規矩,不想看文章的同窗能夠直接移步到Githubgit

首先跟你們說聲抱歉,距離上一篇文章CEventCenter將近一年了,最近才稍微有點空閒的時間能夠寫寫博客,工做實在太忙,抱歉哈。github

近期在開源一款即時通信App,因爲以前發佈的NettyChat屬於封裝的一個Module,不少想基於Netty+TCP+Protobuf開發IM類App的同窗不知道要怎麼上手,並且羣裏以及掘金上也有不少同窗想要聊天類的UI以及消息持久化、離線消息之類的處理邏輯代碼等,因此決定從零開始,帶領你們開發一款優秀的IM App,會包含ims_kula(基礎通訊模塊)、KulaChat(基於ims開發的App)以及kulachat-server(Java服務端),將會是一個完整項目,敬請期待~express

相信很多同窗都踩過Android系統鍵盤處理的坑,尤爲是本身開發過IM App的同窗,在處理聊天會話頁的鍵盤彈起、表情切換、輸入法切換、更多模塊切換等,每每會遇到鍵盤擠壓佈局、切換閃動及切換效果比較生硬之類的問題,我也有幸遇到過,從網上找了不少種方法,但效果不盡如人意,因而決定本身本身動手擼一個。接下來,我將帶領你們,算了,廢話很少說,咱們直接開始。微信

效果對比

  • 微信

微信鍵盤切換效果

  • KulaKeyboard

KulaKeyboard鍵盤切換效果

gif質量比較差,你們將就着看一下。從以上效果對比,咱們能夠注意幾個點:markdown

  1. 鍵盤首次彈出時,是有動畫效果的,能夠看到RecyclerView也跟着向上平移;
  2. 從輸入法到表情切換時,因爲表情面板比輸入法稍高,能夠看到鍵盤消失的同時,表情面板顯示了,同時RecyclerView再總體向上平移了一段距離;
  3. 同上,從表情面板切換到輸入法時,也有動畫效果;
  4. 輸入法或表情面板收起時,RecyclerView向下平移;
  5. 表情面板右下角,因爲須要顯示發送及刪除按鈕,因此最後兩行的最後兩個表情老是隱藏的,除非滑動到底部才所有顯示。

接下來,咱們來分析一下細節。app

細節分析

鍵盤高度獲取

windowSoftInputMode

在AndroidManifest.xml的節點,能夠設置windowSoftInputMode屬性,取值分別是如下10種:ide

  • stateUnspecified
  • stateUnchanged
  • stateHidden
  • stateAlwaysHidden
  • stateVisible
  • stateAlwaysVisible
  • adjustUnspecified
  • adjustResize
  • adjustPan
  • adjustNothing

其中,以state開頭的都是設置軟鍵盤的顯示與隱藏的模式,咱們無須關心,咱們須要關心的是後面4個以adjust開頭的屬性,這4個屬性是設置軟鍵盤與顯示內容之間的關係,下面咱們來分析一下這4個屬性,以及分別設置一下看看效果:工具

  • adjustUnspecified
    • 說明
      默認值。不指定是否Activity的主窗口是否調整大小來爲軟鍵盤騰出空間或是否平移窗口內容來顯示內容焦點(EditText)。若是窗口內容存在可滾動的控件(好比RecyclerView),那麼系統將會選擇adjustResize模式將窗口調整大小(重繪RecyclerView)。若是不存在可滾動的控件,那麼系統將會將窗口總體向上平移以顯示軟鍵盤。也就是說,若是windowSoftInputMode設置爲adjustUnspecified或者不指定任何屬性時,系統將會在adjustResizeadjustPan中選擇合適的一種
    • 效果展現
      • 不包含可滾動的控件
        adjustUnspecified不包含可滾動的控件
      • 包含可滾動的控件
        adjustUnspecified包含可滾動的控件
  • adjustResize
    • 說明
    Activity的主窗口老是調整大小來爲軟鍵盤騰出空間。若是主窗口存在可滑動的控件,那麼系統將會調整該控件大小。若是不存在可滑動的控件,那麼系統將會使主窗口布局進行壓縮。
    • 效果展現
      • 不包含可滾動的控件
        adjustUnspecified不包含可滾動的控件
      • 包含可滾動的控件
        adjustUnspecified包含可滾動的控件
  • adjustPan
    • 說明
    主窗口的內容焦點(EditText)若是處在軟鍵盤的高度覆蓋的區域時,主窗口自動向上平移直至軟鍵盤不遮擋內容焦點爲止,使用戶總能看到輸入內容的部分。
    • 效果展現
      • 不包含可滾動的控件
        adjustUnspecified不包含可滾動的控件
      • 包含可滾動的控件
        adjustUnspecified包含可滾動的控件
  • adjustNothing
    • 說明
    不做任何反應,不關心軟鍵盤是否遮擋內容焦點(EditText)。
    • 效果展現
      • 不包含可滾動的控件
        adjustUnspecified不包含可滾動的控件
      • 包含可滾動的控件
        adjustUnspecified包含可滾動的控件

以上屬性說明,大部分參照網上的介紹加入本身的理解,但願能通俗易懂。oop

切換動畫

從上述屬性說明及效果展現能夠看到,雖然設置adjustResize能夠實現軟鍵盤彈出及輸入面板切換到表情面板時Activity主窗口的RecyclerView經過重繪去調整大小以適應,但同時也能夠看到軟鍵盤彈出時幾乎沒有任何動畫過渡效果,界面切換很是生硬。固然網上也有很多人的實現方式是使表情面板高度和軟鍵盤一致,這樣在輸入法面板和表情面板來回切換時避免界面切換效果比較生硬的問題,但軟鍵盤彈出的效果仍是很是生硬。

那麼,有沒有一種方法可使切換效果更天然、體驗更好呢?答案是確定的。賣個關子,先聽我把最後一個點說完,咱們再來分析一下怎麼實現。

利用不可見的PopupWindow獲取鍵盤打開狀態及鍵盤高度

Android系統沒有提供API可讓咱們獲取鍵盤高度,因此只能另想辦法。目前比較主流的方案是經過OnGlobalLayoutListener的方式獲取,若是windowSoftInputMode設置爲adjustUnspecified | adjustPan | adjustResize其中一種,那麼鍵盤彈出時佈局將會重繪或平移,相關的onGlobalLayout()也將會回調,而後再作相關的計算便可獲得鍵盤實際高度。

注:若是windowSoftInputMode設置爲adjustNothing,在鍵盤彈出時,onGlobalLayout不會回調。

爲何須要利用不可見的PopupWindow獲取鍵盤打開狀態及鍵盤高度呢?

首先咱們來分析一下windowSoftInputMode的屬性,設置爲adjustUnspecified 時,系統會根據控件類型選擇adjustPanadjustResize其中一種。adjustPan的效果是鍵盤彈出時,整個佈局向上平移一段距離,這不是咱們想要的(由於可能會有TopBar的存在),而adjustResize貌似能夠實現咱們想要的效果,但是切換太生硬,幾乎沒有任何動畫過渡。

綜上所述,咱們能夠利用adjustNothing屬性,讓系統不要干預,咱們本身來實現彈出鍵盤時佈局指定控件向上平移的效果。向上平移多少像素呢?這個時候就須要獲取到鍵盤高度,使鍵盤彈出時,佈局向上平移鍵盤的高度便可。

說到這裏,你們應該注意到了上面的備註:若是windowSoftInputMode設置爲adjustNothing,在鍵盤彈出時,onGlobalLayout()不會回調

那麼有沒有辦法能夠把activity的windowSoftInputMode屬性設置爲adjustNothing而且能夠獲取鍵盤高度呢?答案是確定的,這裏須要用到一些小技巧:在Activity打開時,同時建立一個高度爲match_parent,寬度爲0PopupWindow,寬度爲0時,PopupWindow是不可見的,但該PopupWindow確實存在,因爲高度爲match_parent,因此在PopupWindow裏設置OnGlobalLayoutListener,再經過計算,便可獲取鍵盤高度。至於鍵盤打開狀態,那就很是簡單了,能夠在onGlobalLayout()中判斷鍵盤高度大於必定高度(好比整個屏幕高度的1/3)的時候即認爲鍵盤爲打開狀態,反之即認爲鍵盤收起。

好了,說完了原理,咱們來看看具體實現方式。

具體實現

咱們先看看實現思路:假設有一個ChatActivity,頂部爲TopBar,主體部分爲RecyclerView,頂部爲輸入框,佈局代碼以下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/layout_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:clipChildren="false"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/top_bar">

        <LinearLayout
            android:id="@+id/layout_body"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recycler_view"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:background="#333333" />

            <com.freddy.kulakeyboard.sample.CInputPanel
                android:id="@+id/chat_input_panel"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </LinearLayout>

        <com.freddy.kulakeyboard.sample.CExpressionPanel
            android:id="@+id/expression_panel"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="invisible" />

        <com.freddy.kulakeyboard.sample.CMorePanel
            android:id="@+id/more_panel"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="invisible" />
    </LinearLayout>

    <TextView
        android:id="@+id/top_bar"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:background="#00bfcf"
        android:gravity="center"
        android:textStyle="bold"
        android:text="TopBar"
        android:textColor="#000000"
        android:textSize="18sp"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
複製代碼

仔細觀察,能夠看到第一個LinearLayout設置了一個屬性:android:clipChildren="false",有什麼做用呢?官方文檔的解釋是:Defines whether a child is limited to draw inside of its bounds or not. 渣渣翻譯過來的意思是:用來定義他的子控件是否要在他應有的邊界內進行繪製,簡單地說,也就是是否容許子View超出父佈局的邊界

畫個圖比較直觀:

KulaKeyboard實現細節分析

如圖,暗藍色區域爲手機屏幕,包含TopBarRecyclerViewInputPanel,紫色區域爲表情面板、更多面板等,顯示在屏幕區域外。因爲設置了android:clipChildren="false"屬性,因此紫色區域不受父佈局邊界限制,得以顯示在屏幕區域外。鍵盤打開時,TopBar保持不動,RecyclerViewInputPanel及紫色區域面板向上平移鍵盤高度,同理,鍵盤收起時這些控件向下平移鍵盤高度,顯示到屏幕區域外,便可實現咱們想要的動畫過渡效果。

分析完實現方式,咱們來看看具體代碼實現。

因爲貼所有代碼篇幅過長,因此只貼關鍵部分代碼,具體實現你們能夠到Github查看。

  1. 定義IPanel接口
interface IPanel {
    	/**
        * 重置狀態
        */
       fun reset()

       /**
        * 獲取面板高度
        */
       fun getPanelHeight(): Int
   }
複製代碼
  1. 對於InputPanel,須要定義特定的接口
interface IInputPanel : IPanel {

    /**
     * 軟鍵盤打開
     */
    fun onSoftKeyboardOpened()

    /**
     * 軟件盤關閉
     */
    fun onSoftKeyboardClosed()

    /**
     * 設置佈局動畫處理監聽器
     */
    fun setOnLayoutAnimatorHandleListener(listener: ((panelType: PanelType, lastPanelType: PanelType, fromValue: Float, toValue: Float) -> Unit)?)

    /**
     * 設置輸入面板(包括軟鍵盤、語音、表情、更多等)狀態改變監聽器
     */
    fun setOnInputStateChangedListener(listener: OnInputPanelStateChangedListener?)
}
複製代碼
  1. OnInputPanelStateChangedListener
interface OnInputPanelStateChangedListener {

    /**
     * 顯示語音面板
     */
    fun onShowVoicePanel()

    /**
     * 顯示軟鍵盤面板
     */
    fun onShowInputMethodPanel()

    /**
     * 顯示錶情面板
     */
    fun onShowExpressionPanel()

    /**
     * 顯示更多面板
     */
    fun onShowMorePanel()
}
複製代碼
  1. PanelTyoe
enum class PanelType {

    /**
     * 面板類型:軟鍵盤
     */
    INPUT_MOTHOD,

    /**
     * 面板類型:語音
     */
    VOICE,

    /**
     * 面板類型:表情
     */
    EXPRESSION,

    /**
     * 面板類型:更多
     */
    MORE,

    /**
     * 面板類型:無
     */
    NONE
}
複製代碼
  1. KeyboardStatePopupWindow
class KeyboardStatePopupWindow(var context: Context, anchorView: View) : PopupWindow(),
    ViewTreeObserver.OnGlobalLayoutListener {

    init {
        val contentView = View(context)
        setContentView(contentView)
        width = 0
        height = ViewGroup.LayoutParams.MATCH_PARENT
        setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
        softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
        inputMethodMode = INPUT_METHOD_NEEDED
        contentView.viewTreeObserver.addOnGlobalLayoutListener(this)

        anchorView.post {
            showAtLocation(
                anchorView,
                Gravity.NO_GRAVITY,
                0,
                0
            )
        }
    }

    private var maxHeight = 0
    private var isSoftKeyboardOpened = false

    override fun onGlobalLayout() {
        val rect = Rect()
        contentView.getWindowVisibleDisplayFrame(rect)
        if (rect.bottom > maxHeight) {
            maxHeight = rect.bottom
        }
        val screenHeight: Int = DensityUtil.getScreenHeight(context)
        //鍵盤的高度
        val keyboardHeight = maxHeight - rect.bottom
        val visible = keyboardHeight > screenHeight / 4
        if (!isSoftKeyboardOpened && visible) {
            isSoftKeyboardOpened = true
            onKeyboardStateListener?.onOpened(keyboardHeight)
            KulaKeyboardHelper.keyboardHeight = keyboardHeight
        } else if (isSoftKeyboardOpened && !visible) {
            isSoftKeyboardOpened = false
            onKeyboardStateListener?.onClosed()
        }
    }

    fun release() {
        contentView.viewTreeObserver.removeOnGlobalLayoutListener(this)
    }

    private var onKeyboardStateListener: OnKeyboardStateListener? = null

    fun setOnKeyboardStateListener(listener: OnKeyboardStateListener?) {
        this.onKeyboardStateListener = listener
    }

    interface OnKeyboardStateListener {
        fun onOpened(keyboardHeight: Int)
        fun onClosed()
    }
}
複製代碼

接下來,就是關鍵的KeyboardHelper啦,代碼比較簡單,註釋就懶得寫了

class KeyboardHelper {

    private lateinit var context: Context
    private var rootLayout: ViewGroup? = null
    private var bodyLayout: ViewGroup? = null
    private var inputPanel: IInputPanel? = null
    private var expressionPanel: IPanel? = null
    private var morePanel: IPanel? = null
    private var keyboardStatePopupWindow: KeyboardStatePopupWindow? = null

    companion object {
        var keyboardHeight = 0
        var inputPanelHeight = 0
        var expressionPanelHeight = 0
        var morePanelHeight = 0
    }

    fun init(context: Context): KeyboardHelper {
        this.context = context
        return this
    }

    fun reset() {
        inputPanel?.reset()
        expressionPanel?.reset()
    }

    fun release() {
        inputPanel?.reset()
        inputPanel = null
        expressionPanel?.reset()
        expressionPanel = null
        keyboardStatePopupWindow?.dismiss()
        keyboardStatePopupWindow = null
    }

    fun setKeyboardHeight(keyboardHeight: Int): KeyboardHelper {
        KeyboardHelper.keyboardHeight = keyboardHeight
        if (inputPanelHeight == 0) {
            inputPanelHeight = keyboardHeight
        }
        return this
    }

    fun bindRootLayout(rootLayout: ViewGroup): KeyboardHelper {
        this.rootLayout = rootLayout
        keyboardStatePopupWindow = KeyboardStatePopupWindow(context, rootLayout)
        keyboardStatePopupWindow?.setOnKeyboardStateListener(object :
            KeyboardStatePopupWindow.OnKeyboardStateListener {
            override fun onOpened(keyboardHeight: Int) {
                KeyboardHelper.keyboardHeight = keyboardHeight
                inputPanel?.onSoftKeyboardOpened()
                onKeyboardStateListener?.onOpened(keyboardHeight)
                inputPanel?.apply {
                    inputPanelHeight = getPanelHeight()
                }
                expressionPanel?.apply {
                    expressionPanelHeight = getPanelHeight()
                }
                morePanel?.apply {
                    morePanelHeight = getPanelHeight()
                }
            }

            override fun onClosed() {
                inputPanel?.onSoftKeyboardClosed()
                onKeyboardStateListener?.onClosed()
            }
        })
        return this
    }

    fun bindBodyLayout(bodyLayout: ViewGroup): KeyboardHelper {
        this.bodyLayout = bodyLayout
        return this
    }

    fun <P : IPanel> bindVoicePanel(panel: P): KeyboardHelper {
        return this
    }

    fun <P : IInputPanel> bindInputPanel(panel: P): KeyboardHelper {
        this.inputPanel = panel
        inputPanelHeight = panel.getPanelHeight()
        panel.setOnInputStateChangedListener(object : OnInputPanelStateChangedListener {
            override fun onShowVoicePanel() {
                if (expressionPanel !is ViewGroup || morePanel !is ViewGroup) return
                expressionPanel?.let {
                    it as ViewGroup
                    it.visibility = View.GONE
                }
                morePanel?.let {
                    it as ViewGroup
                    it.visibility = View.GONE
                }
            }

            override fun onShowInputMethodPanel() {
                if (expressionPanel !is ViewGroup || morePanel !is ViewGroup) return
                expressionPanel?.let {
                    it as ViewGroup
                    it.visibility = View.GONE
                }
                morePanel?.let {
                    it as ViewGroup
                    it.visibility = View.GONE
                }
            }

            override fun onShowExpressionPanel() {
                if (expressionPanel !is ViewGroup) return
                expressionPanel?.let {
                    it as ViewGroup
                    it.visibility = View.VISIBLE
                }
            }

            override fun onShowMorePanel() {
                if (morePanel !is ViewGroup) return
                morePanel?.let {
                    it as ViewGroup
                    it.visibility = View.VISIBLE
                }
            }
        })
        panel.setOnLayoutAnimatorHandleListener { panelType, lastPanelType, fromValue, toValue ->
            handlePanelMoveAnimator(panelType, lastPanelType, fromValue, toValue)
        }
        return this
    }

    fun <P : IPanel> bindExpressionPanel(panel: P): KeyboardHelper {
        this.expressionPanel = panel
        expressionPanelHeight = panel.getPanelHeight()
        return this
    }

    fun <P : IPanel> bindMorePanel(panel: P): KeyboardHelper {
        this.morePanel = panel
        morePanelHeight = panel.getPanelHeight()
        return this
    }

    @SuppressLint("ObjectAnimatorBinding")
    private fun handlePanelMoveAnimator(panelType: PanelType, lastPanelType: PanelType, fromValue: Float, toValue: Float) {
        Log.d("KulaKeyboardHelper", "panelType = $panelType, lastPanelType = $lastPanelType")
        val bodyLayoutTranslationYAnimator: ObjectAnimator =
            ObjectAnimator.ofFloat(bodyLayout, "translationY", fromValue, toValue)
        var panelTranslationYAnimator: ObjectAnimator? = null
        when(panelType) {
            PanelType.INPUT_MOTHOD -> {
                expressionPanel?.reset()
                morePanel?.reset()
            }
            PanelType.VOICE -> {
                expressionPanel?.reset()
                morePanel?.reset()
            }
            PanelType.EXPRESSION -> {
                morePanel?.reset()
                panelTranslationYAnimator = ObjectAnimator.ofFloat(expressionPanel, "translationY", fromValue, toValue)
            }
            PanelType.MORE -> {
                expressionPanel?.reset()
                panelTranslationYAnimator = ObjectAnimator.ofFloat(morePanel, "translationY", fromValue, toValue)
            }
            else -> {}
        }
        val animatorSet = AnimatorSet()
        animatorSet.duration = 250
        animatorSet.interpolator = DecelerateInterpolator()
        if(panelTranslationYAnimator == null) {
            animatorSet.play(bodyLayoutTranslationYAnimator)
        }else {
            animatorSet.play(bodyLayoutTranslationYAnimator).with(panelTranslationYAnimator)
        }
        animatorSet.addListener(object : Animator.AnimatorListener {
            override fun onAnimationStart(animation: Animator) {}
            override fun onAnimationEnd(animation: Animator) {
                bodyLayout?.requestLayout()
                expressionPanel?.let {
                    it as ViewGroup
                    it.requestLayout()
                }
                morePanel?.let {
                    it as ViewGroup
                    it.requestLayout()
                }
            }

            override fun onAnimationCancel(animation: Animator) {}
            override fun onAnimationRepeat(animation: Animator) {}
        })
        animatorSet.start()
    }

    private var onKeyboardStateListener: OnKeyboardStateListener? = null
    fun setOnKeyboardStateListener(listener: OnKeyboardStateListener?): KeyboardHelper {
        this.onKeyboardStateListener = listener
        return this
    }

    interface OnKeyboardStateListener {
        fun onOpened(keyboardHeight: Int)
        fun onClosed()
    }
}
複製代碼

最後,貼上兩個工具類的代碼 DensityUtil

object DensityUtil {

    /**
     * 根據手機的分辨率從 dp 的單位 轉成爲 px(像素)
     *
     * @param dpValue
     * @return
     */
    fun dp2px(context: Context, dpValue: Float): Int {
        return (dpValue * getDisplayMetrics(context).density).roundToInt()
    }

    /**
     * 根據手機的分辨率從 px(像素) 的單位 轉成爲 dp
     *
     * @param pxValue
     * @return
     */
    fun px2dp(context: Context, pxValue: Float): Int {
        return (pxValue / getDisplayMetrics(context).density).roundToInt()
    }

    /**
     * sp轉px
     *
     * @param spVal
     * @return
     */
    fun sp2px(context: Context, spVal: Float): Int {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP,
            spVal, context.resources.displayMetrics
        ).roundToInt()
    }

    /**
     * px轉sp
     *
     * @param pxVal
     * @return
     */
    fun px2sp(context: Context, pxVal: Float): Float {
        return pxVal / getDisplayMetrics(context).scaledDensity
    }

    private fun getDisplayMetrics(context: Context): DisplayMetrics {
        val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val display = wm.defaultDisplay
        val metrics = DisplayMetrics()
        display.getMetrics(metrics)
        return metrics
    }

    /**
     * 獲取屏幕寬度
     * @return
     */
    fun getScreenWidth(context: Context): Int {
        return getDisplayMetrics(context).widthPixels
    }

    /**
     * 獲取屏幕高度
     * @return
     */
    fun getScreenHeight(context: Context): Int {
        return getDisplayMetrics(context).heightPixels
    }

    /**
     * 獲取像素密度
     * @return
     */
    fun getDensity(context: Context): Float {
        return getDisplayMetrics(context).density
    }
}
複製代碼

UIUtil

object UIUtil {

    /**
     * 使控件獲取焦點
     *
     * @param view
     */
    fun requestFocus(view: View?) {
        if (view != null) {
            view.isFocusable = true
            view.isFocusableInTouchMode = true
            view.requestFocus()
        }
    }

    /**
     * 使控件失去焦點
     *
     * @param view
     */
    fun loseFocus(view: View?) {
        if (view != null) {
            val parent = view.parent as ViewGroup
            parent.isFocusable = true
            parent.isFocusableInTouchMode = true
            parent.requestFocus()
        }
    }

    /**
     * 是否應該隱藏鍵盤
     *
     * @param v
     * @param event
     * @return
     */
    fun isShouldHideInput(v: View?, event: MotionEvent): Boolean {
        if (v != null && v is EditText) {
            val leftTop = intArrayOf(0, 0)
            //獲取輸入框當前的location位置
            v.getLocationInWindow(leftTop)
            val left = leftTop[0]
            val top = leftTop[1]
            val bottom = top + v.getHeight()
            val right = left + v.getWidth()
            return !(event.x > left && event.x < right && event.y > top && event.y < bottom)
        }
        return false
    }

    /**
     * 隱藏鍵盤
     *
     * @param context
     * @param v       輸入框
     */
    fun hideSoftInput(context: Context, v: View) {
        val imm = context
            .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(v.applicationWindowToken, 0)
    }

    fun showSoftInput(context: Context, v: View?) {
        val imm =
            context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.showSoftInput(v, 0)
    }
}
複製代碼

至此,實現的代碼已經所有貼出,對,就是這麼簡單~

調用方式

private lateinit var keyboardHelper: KeyboardHelper
keyboardHelper = KeyboardHelper()
        keyboardHelper.init(this)
            .bindRootLayout(layout_main)
            .bindBodyLayout(layout_body)
            .bindInputPanel(chat_input_panel)
            .bindExpressionPanel(expression_panel)
            .bindMorePanel(more_panel)
            .setKeyboardHeight(
                if (App.instance.keyboardHeight == 0) DensityUtil.getScreenHeight(applicationContext) / 5 * 2 else App.instance.keyboardHeight
            )
            .setOnKeyboardStateListener(object : KeyboardHelper.OnKeyboardStateListener {
                override fun onOpened(keyboardHeight: Int) {
                    App.instance.keyboardHeight = keyboardHeight
                }

                override fun onClosed() {
                }
            })
複製代碼

因爲篇幅過長,至於更詳細的調用方式和自定義的CInputPanelCExpressionPanelCMorePanel,在此就不貼了,你們能夠跳轉至Github參考,README.md將詳細講解調用方式及自定義須要的Panel。

寫在最後

Github地址
終於寫完啦,原本這一塊的代碼在KulaChat App裏面,考慮到有不少同窗本身開發IM App,須要實現鍵盤切換效果,因此就單獨把鍵盤切換封裝成一個Module,項目中有一個Emoji表情的面板實現,支持自定義各類表情面板以及更多面板等Github上面有詳細的使用方式,若是項目對您有幫助,麻煩點個star,同時歡迎fork和pull request,期待你們與我一塊兒共同完善,爲開源社區貢獻一點力量。

PS:新開的公衆號不能留言,若是你們有不一樣的意見或建議,能夠到掘金上評論或者加到QQ羣:1015178804,若是羣滿人的話,也能夠在公衆號給我私信,謝謝。
貼上公衆號:
FreddyChen
FreddyChen的微信公衆號

相關文章
相關標籤/搜索