Android仿微信键盘

1,532 阅读5分钟

最终效果

原理

主要是通过将面板视图显示在屏幕外,通过PopupWindow代理监听键盘的弹起及隐藏。动态设置面板高度并且平移面板视图。

布局

布局主要是用了约束布局ConstraintLayou,并且使用了2.0中的自定义ConstraintHelper和Flow减少页面嵌套层级

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ededed"
        tools:context=".MainActivity">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:background="@mipmap/bg"
            android:layout_height="0dp"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toTopOf="@+id/sendMessageFlow"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.testapp.keyboardutil.MyFlow
            android:id="@+id/sendMessageFlow"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:background="#fefefe"
            android:padding="10dp"
            app:constraint_referenced_ids="voiceSwitchIv,sendEdt,switchBut"
            app:flow_horizontalGap="10dp"
            app:flow_wrapMode="none"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"></com.testapp.keyboardutil.MyFlow>

        <ImageView
            android:id="@+id/voiceSwitchIv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:adjustViewBounds="true"
            android:src="@mipmap/chatting_voice_btn_icon"
            tools:ignore="MissingConstraints" />

        <EditText
            android:id="@+id/sendEdt"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:background="@drawable/chatting_voice_bg"
            android:gravity="center_vertical|left"
            android:minHeight="38dp"
            android:padding="5dp"
            android:textColor="@android:color/black"
            android:textSize="14sp"
            android:visibility="visible"
            tools:ignore="MissingConstraints,TextFields"
            tools:layout_editor_absoluteX="32dp"
            tools:layout_editor_absoluteY="685dp" />

        <ImageView
            android:id="@+id/switchBut"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:adjustViewBounds="true"
            android:clickable="true"
            android:src="@mipmap/chatting_plus_btn_icon"
            android:visibility="visible"
            tools:ignore="MissingConstraints" />

        <TextView
            android:id="@+id/sendVoiceBtn"
            android:layout_width="0dp"
            android:layout_height="38dp"
            android:background="@drawable/chatting_voice_bg"
            android:clickable="true"
            android:gravity="center"
            android:soundEffectsEnabled="true"
            android:text="按住说话"
            android:textColor="#b3b3b3"
            android:textSize="16sp"
            android:visibility="gone"
            app:layout_constraintBottom_toBottomOf="@+id/sendEdt"
            app:layout_constraintEnd_toEndOf="@+id/sendEdt"
            app:layout_constraintStart_toStartOf="@+id/sendEdt"
            app:layout_constraintTop_toTopOf="@+id/sendEdt" />


        <com.testapp.keyboardutil.MyFlow
            android:id="@+id/panelFlow"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="#ffffff"
            android:padding="10dp"
            app:constraint_referenced_ids="chatAlbum,chatCapture,chatContact,chatLocation,chatFile,chatCloud"
            app:flow_horizontalGap="10dp"
            app:flow_maxElementsWrap="4"
            app:flow_verticalGap="10dp"
            android:visibility="invisible"
            app:flow_wrapMode="aligned"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/sendMessageFlow"></com.testapp.keyboardutil.MyFlow>


        <View
            android:id="@+id/lineView"
            android:layout_width="match_parent"
            android:layout_height="1px"
            android:background="#d5d3d5"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/sendMessageFlow" />

        <TextView
            android:id="@+id/chatAlbum"
            style="@style/text_function_item"
            android:drawableTop="@mipmap/icon_album"
            android:drawablePadding="5dp"
            android:gravity="center"
            android:text="相册"
            tools:ignore="MissingConstraints" />

        <TextView
            android:id="@+id/chatCapture"
            style="@style/text_function_item"
            android:drawableTop="@mipmap/icon_capture"
            android:drawablePadding="5dp"
            android:gravity="center"
            android:text="拍照"
            tools:ignore="MissingConstraints" />

        <TextView
            android:id="@+id/chatContact"
            style="@style/text_function_item"
            android:drawableTop="@mipmap/icon_im_contact"
            android:drawablePadding="5dp"
            android:gravity="center"
            android:text="联系人"
            tools:ignore="MissingConstraints" />


        <TextView
            android:id="@+id/chatLocation"
            style="@style/text_function_item"
            android:drawableTop="@mipmap/icon_im_location"
            android:drawablePadding="5dp"
            android:gravity="center"
            android:text="位置"
            tools:ignore="MissingConstraints" />

        <TextView
            android:id="@+id/chatFile"
            style="@style/text_function_item"
            android:drawableTop="@mipmap/icon_im_file"
            android:drawablePadding="5dp"
            android:gravity="center"
            android:text="文件"
            tools:ignore="MissingConstraints" />

        <TextView
            android:id="@+id/chatCloud"
            style="@style/text_function_item"
            android:drawableTop="@mipmap/icon_cloud"
            android:drawablePadding="5dp"
            android:gravity="center"
            android:text="我的资料"
            tools:ignore="MissingConstraints" />


        <com.testapp.keyboardutil.LayoutHelper
            android:id="@+id/layoutHelper"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:constraint_referenced_ids="recyclerView,sendMessageFlow,lineView,panelFlow"
            tools:ignore="MissingConstraints"></com.testapp.keyboardutil.LayoutHelper>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

PopupWindow代理监听键盘变化

android中PopupWindow具有单独的键盘行为,可以通过setSoftInputMode(int mode)方法设置PopupWindow键盘模式。所以我们把activity的键盘模式设置为android:windowSoftInputMode="adjustNothing",让activity不再响应键盘,然后通过设置一个高度为MATCH_PARENT宽度为0的PopupWindow去代理监听。

class KeyboardHeightProvider(view: View, val function: (keyboardHeight: Int) -> Unit) :
    PopupWindow(view.context),
    ViewTreeObserver.OnGlobalLayoutListener {

    //当前PopupWindow最大的显示高度
    private var maxHeight = 0

    init {
        contentView = View(view.context)
        width = 0
        height = ViewGroup.LayoutParams.MATCH_PARENT
        //设置背景
        setBackgroundDrawable(ColorDrawable(0))
        //设置键盘弹出模式
        softInputMode =
            WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
        inputMethodMode = INPUT_METHOD_NEEDED
        //设置监听
        contentView.viewTreeObserver.addOnGlobalLayoutListener(this)
        //显示弹窗
        view.post {
            showAtLocation(
                view,
                Gravity.NO_GRAVITY,
                0,
                0
            )
        }
    }

    override fun onGlobalLayout() {
        val rect = Rect()
        contentView.getWindowVisibleDisplayFrame(rect)
        if (rect.bottom > maxHeight) {
            maxHeight = rect.bottom
        }
        //键盘的高度
        val keyboardHeight = maxHeight - rect.bottom
        function(keyboardHeight)
    }


}

面板的隐藏和显示

规定弹出来面板view未显示的时候View.INVISIBLE,只有弹出并且键盘隐藏时才显示View.VISIBLE。首先初始化键盘

    /**
     * 键盘初始化
     */
    fun init(
        layoutHelper: LayoutHelper,
        switch: View, //切换按钮
        edt: EditText,//输入框
        panelView: MyFlow, //面板view
        function: ((isShowPanel: Boolean) -> Unit)? = null
    ) {
        setListener(panelView, layoutHelper)

        switch.setOnClickListener {
            //只有面板view展开时才可见
            if (panelView.visibility == View.VISIBLE) {
                //显示键盘
                panelView.visibility = View.INVISIBLE
                showKeyboard(edt)
            } else {
                //显示面板view
                showPanel(layoutHelper, panelView, edt)
                edt.clearFocus()
            }
            function?.invoke(panelView.visibility == View.VISIBLE)
        }

        edt.setOnTouchListener { v, event ->
            if (event.action == MotionEvent.ACTION_UP) {
                val keyboardHeight = -getKeyboardHeight(
                    layoutHelper.context
                )
                layoutHelper.translationY = keyboardHeight.toFloat()
                panelView.visibility = View.INVISIBLE
            }
            false
        }


    }

通过切换按钮设置键盘弹出或者隐藏,如果面板当前显示就隐藏面板显示键盘,否则就根据当前面板是否展开来平移布局。

显示面板是否平移

    /**
     * 显示面板
     */
    private fun showPanel(layoutHelper: LayoutHelper, panelView: View, edt: EditText) {
        if (layoutHelper.isExpand) {
            //已经展开 显示面板view隐藏键盘
            panelView.visibility = View.VISIBLE
            hideKeyboard(edt)
        } else {
            //没有展开  直接展开显示面板view
            panelView.visibility = View.VISIBLE
            val keyboardHeight = -getKeyboardHeight(
                layoutHelper.context
            )
            layoutHelper.translationY = keyboardHeight.toFloat()
        }
    }

代码显示键盘和隐藏键盘

    /**
     * 显示键盘
     */
    private fun showKeyboard(view: View) {
        view.requestFocus()
        val inputManager =
            view.context.getSystemService(
                Context.INPUT_METHOD_SERVICE
            ) as InputMethodManager
        inputManager.showSoftInput(view, 0)
    }

    /**
     * 隐藏键盘
     */
    private fun hideKeyboard(view: View) {
        view.clearFocus()
        val imm = view.context
            .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(view.windowToken, 0)
    }

LayoutHelper是自定义ConstraintHelper,ConstraintHelper主要作用是引用其他view,方便封装一些特定的行为。并且不会增加view的层级有利性能。 在LayoutHelper中,重写了setTranslationY()方法,使其引用的view一起动画平移。

class LayoutHelper @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {

    private var mContainer: ConstraintLayout? = null

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        mContainer = this.parent as ConstraintLayout
        initData()
    }

    private fun initData() {
        val views = getViews(mContainer)
        anim.duration = 300
        anim.addUpdateListener { animation ->
            val value = animation.animatedValue as Float
            views.map { view ->
                view.translationY = value
            }
        }

    }

    private val anim = ValueAnimator()

    //是否展开
    val isExpand: Boolean
        get() = previousY != 0f

    private var previousY = 0f //上次平移距离
    override fun setTranslationY(translationY: Float) {
        mContainer?.also {
            anim.setFloatValues(previousY, translationY)
            previousY = translationY
            anim.start()
        }
    }


}

Flow(流式布局)在ConstraintLayout2.0中添加,官方封装的ContraintHelper,主要作用是可以像chian一样将引用的view水平或垂直放置 MyFlow中重写了setTranslationYsetVisibility方法,让其平移或显示时,自己和引用的view同时生效。

class MyFlow @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : Flow(context, attrs, defStyleAttr) {


    override fun setTranslationY(translationY: Float) {
        val mContainer = this.parent as ConstraintLayout
        getViews(mContainer).map { v ->
            v.translationY = translationY
        }
        super.setTranslationY(translationY)
    }


    override fun setVisibility(visibility: Int) {
        super.setVisibility(visibility)
        val mContainer = this.parent as ConstraintLayout
        getViews(mContainer).map { v ->
            v.visibility = visibility
        }
    }

}

然后就是设置监听键盘高度变化

   private fun setListener(
        panelView: MyFlow,
        layoutHelper: LayoutHelper
    ) {
        KeyboardHeightProvider(panelView) { keyboardHeight ->
            val isKeyboardshow = keyboardHeight > 0
            //计算并保存键盘高度
            calculateKeyboardHeight(panelView,layoutHelper, keyboardHeight)
            if (isKeyboardshow != lastKeyboardshow) {
                changedPanelAndKeyboard(isKeyboardshow, layoutHelper, panelView)
            }
            lastKeyboardshow = isKeyboardshow


        }
    }

通过calculateKeyboardHeigh方法,保存当前键盘高度并动态修改面板view的高度。

    /**
     * 计算并保存键盘高度
     */
    private fun calculateKeyboardHeight(
        panelView: MyFlow,
        layoutHelper: LayoutHelper,
        keyboardHeight: Int
    ) {
        val height = getKeyboardHeight(panelView.context)
        //如果面板高度和键盘不同
        if (panelView.height != height) {
            //改变view高度
            changedViewHeight(panelView, height)
        }
        //保存键盘高度
        val changed =
            saveKeyboardHeight(panelView.context, keyboardHeight)
        if (changed) {
            var height = getKeyboardHeight(panelView.context)
            //改变view高度
            changedViewHeight(
                panelView,
                height
            )
            //面板是展开的时候 键盘变化需要平移布局
            if (layoutHelper.isExpand){
                layoutHelper.translationY= -height.toFloat()
            }

        }


    }

注意当键盘展开的时候,动态改变键盘高度时我们需要平移布局使其随之改变。保存键盘高度

    /**
     * 保存键盘高度
     */
    private fun saveKeyboardHeight(context: Context, keyboardHeight: Int): Boolean {
        if (lastKeyboard == keyboardHeight) {
            return false
        }
        if (keyboardHeight <= 0) {
            return false
        }
        lastKeyboard = keyboardHeight
        return KeyBoardSharedPreferences.setHeight(
            context,
            keyboardHeight
        )
    }

获取键盘高度

    /**
     * 获取键盘高度
     */
    private fun getKeyboardHeight(context: Context): Int {
        if (lastKeyboard == 0) {
            lastKeyboard =
                KeyBoardSharedPreferences.getHeight(
                    context
                )

        }
        return lastKeyboard
    }

使用

在activity中直接调用KeyboardUtil.init(binding.layoutHelper, binding.switchBut, binding.sendEdt, binding.panelFlow)就可以了

git地址