最终效果

原理
主要是通过将面板视图显示在屏幕外,通过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中重写了setTranslationY和setVisibility方法,让其平移或显示时,自己和引用的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)就可以了