README: English Doc
在开发聊天/视频/直播/信息流界面时,希望用户在输入法与功能面板(比如表情面板/更多选项面板等)的切换过程中保持平滑过渡。调研了市场上主流的app效果及实现,实现了一套兼容多场景的输入面板切换框架。目前该框架已测试使用。
- 方案1:在 setSoftInputMode = SOFT_INPUT_ADJUST_RESIZE 的场景下,通过ViewTreeObserver.OnGlobalLayoutListener或者ViewCompat.setOnApplyWindowInsetsListener来获取键盘高度,通过修改onLayout的方式调整输入面板的高度。
- 方案2:在 setSoftInputMode = SOFT_INPUT_ADJUST_NOTHING 的场景下,通过ViewCompat.setWindowInsetsAnimationCallback监听键盘过渡动画,获取实时的键盘高度后,通过修改控件translationY的方式实现。
其中方案2是在1.5.0版本中引入的,可以通过android11KeyboardFeature属性控制是否开启Android 11键盘特性(默认是开启的)。在部分应用场景中Android 11键盘特性无法生效,我们会降级成方案1的方式。
备注:由于Android手机设备碎片化严重,可能会出现部分手机的兼容问题,我们为PanelSwitchLayout提供了两个兼容方法,当你的设备无法正常获取到键盘高度时,你可以尝试实现这两个方法来做兼容。
// 针对Android 11以上开启键盘动画特性,高度获取失败时,对外提供兼容方案
var softInputHeightCalculatorOnStart: ((animation: WindowInsetsAnimationCompat, bounds: WindowInsetsAnimationCompat.BoundsCompat) -> Int)? = null
var softInputHeightCalculatorOnProgress: ((insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>) -> Int)? = null
- 改进传统使用
Weight+LinearLayout
动态更改布局高度适配面板的技术方案,支持多种原生 ViewGroup 容器 - 为了追求更平滑的适配效果,当输入法动态调整高度或动态隐藏导航栏时,功能面板能实时适配
- 为了追求更流畅的切换效果,支持滑动模式,滑动会更流畅,同时也支持固定模式
- 丰富的机型适配,适配 全面屏/刘海屏/挖孔屏/Pad 等非常规 Phone 机型
- 丰富的场景支持,支持 Activity/Fragment/Dialog/PopupWindow,应用到聊天/视频/直播/信息流评论等场景
- 丰富的 API 支持,可自定义内容容器,业务面板,灵活控制面板隐藏,灵活控制切换面板速度
- 支持全屏模式,FullScreen 模式下也能处理面板切换
更多细节可参考
Demo 内容如下
从二维码下载 Demo
默认运行 Androidx 版本,如果需要打开非 Androidx,则在 Settings.gradle
中打开 app
并在 gradle.properties
中关闭 Androidx 配置即可。
- 在模块脚本
build.gradle
添加库依赖
1.1、Add it in your root build.gradle at the end of repositories:
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
1.2、Add the dependency
//1.4.0 版本及后续,仅支持 Androidx
dependencies {
implementation 'com.github.DSAppTeam:PanelSwitchHelper:v1.5.12'
}
- 在布局文件 Xml 中使用框架提供的容器
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 不需要被框架处理的布局,可自由布置 -->
<RelativeLayout
android:id="@+id/cus_title_bar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:visibility="gone">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="left|center_vertical"
android:paddingLeft="20dp"
android:text="自定义标题栏"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>
<com.effective.android.panel.view.PanelSwitchLayout
android:id="@+id/panel_switch_layout"
android:layout_width="match_parent"
app:animationSpeed="standard"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 内容区域 -->
<!-- linear_edit_view 指定一个 EditText 用于输入 ,必须项-->
<!-- linear_auto_reset_enable 指定是否 LinearContentContainer 是否能够接受 Touch 事件自动隐藏面板-->
<!-- linear_auto_reset_area 指定是否 LinearContentContainer 只接受某个 View 区域的 Touch 事件来自定隐藏面板-->
<com.effective.android.panel.view.content.LinearContentContainer
android:id="@+id/content_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:edit_view="@id/edit_text">
<com.example.demo.scene.chat.view.HookActionUpRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:id="@+id/bottom_action"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_input_layout"
android:gravity="bottom"
android:minHeight="@dimen/dp_50"
android:orientation="horizontal"
android:paddingLeft="@dimen/dp_10"
android:paddingRight="@dimen/dp_10"
android:paddingBottom="@dimen/dp_7.5">
<!-- 更多入口 -->
<ImageView
android:id="@+id/add_btn"
android:layout_width="@dimen/dp_35"
android:layout_height="@dimen/dp_35"
android:layout_marginRight="@dimen/dp_10"
android:src="@drawable/icon_add" />
<!-- 输入入口 -->
<EditText
android:id="@+id/edit_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_10"
android:layout_marginRight="@dimen/dp_10"
android:layout_weight="1"
android:background="@drawable/selector_edit_focus"
android:imeOptions="actionSearch"
android:maxLines="5"
android:minHeight="@dimen/dp_35"
android:paddingLeft="@dimen/dp_3"
android:paddingTop="@dimen/dp_7.5"
android:paddingRight="@dimen/dp_3"
android:paddingBottom="@dimen/dp_3"
android:textCursorDrawable="@drawable/shape_edit_cursor"
android:textSize="@dimen/sp_16" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="@dimen/dp_35"
android:orientation="horizontal">
<!-- 表情入口 -->
<ImageView
android:id="@+id/emotion_btn"
android:layout_width="@dimen/dp_35"
android:layout_height="@dimen/dp_35"
android:layout_marginEnd="@dimen/dp_10"
android:layout_marginRight="@dimen/dp_10"
android:src="@drawable/selector_emotion_btn" />
<TextView
android:id="@+id/send"
android:layout_width="@dimen/dp_50"
android:layout_height="@dimen/dp_35"
android:background="@drawable/selector_send_btn"
android:gravity="center"
android:text="@string/send"
android:textColor="@color/color_send_btn"
android:textSize="@dimen/sp_15" />
</LinearLayout>
</LinearLayout>
</com.effective.android.panel.view.content.LinearContentContainer>
<!-- 面板区域,仅能包含PanelView-->
<com.effective.android.panel.view.panel.PanelContainer
android:id="@+id/panel_container"
android:layout_width="match_parent"
android:background="@color/common_page_bg_color"
android:layout_height="wrap_content">
<!-- 每一项面板 -->
<!-- panel_layout 用于指定面板该 ID 对应的布局 ,必须项-->
<!-- panel_trigger 用于用户点击该 ID 对应的 View 时切换到该面板 -->
<!-- panel_toggle 用于当该面板显示时 ,用户再次点击 panel_trigger 对应的 View 时是否回切输入法-->
<com.effective.android.panel.view.panel.PanelView
android:id="@+id/panel_emotion"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:panel_layout="@layout/panel_emotion_layout"
app:panel_trigger="@id/emotion_btn" />
<!-- 除了使用框架提供的 PanelView,也可以使用自定义 Panel -->
<com.example.demo.scene.api.CusPanelView
android:id="@+id/panel_addition"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cus_panel_trigger="@id/add_btn"
app:cus_panel_toggle="true"/>
</com.effective.android.panel.view.panel.PanelContainer>
</com.effective.android.panel.view.PanelSwitchLayout>
</LinearLayout>
</layout>
- 初始化 PanelSwitchHelper 对象,框架会自动收集布局信息。同时在返回键会调时拦截处理即可。
//Activity 场景,在 onStart 方法初始化,其他如 Fragment/Dialog/PopupWindow 参考 Demo
private PanelSwitchHelper mHelper;
@Override
protected void onStart() {
super.onStart();
if (mHelper == null) {
mHelper = new PanelSwitchHelper.Builder(this)
.addKeyboardStateListener {
onKeyboardChange {
//可选实现,监听输入法变化
}
}
.addEditTextFocusChangeListener {
onFocusChange { _, hasFocus ->
//可选实现,监听输入框焦点变化
}
}
.addViewClickListener {
onClickBefore {
//可选实现,监听触发器的点击
}
}
.addPanelChangeListener {
onKeyboard {
//可选实现,输入法显示回调
}
onNone {
//可选实现,默认状态回调
}
onPanel {
//可选实现,面板显示回调
}
onPanelSizeChange { panelView, _, _, _, width, height ->
//可选实现,输入法动态调整时引起的面板高度变化动态回调
}
}
.addContentScrollMeasurer { //可选,滑动模式下,可以针对内容面板内的view,定制滑动距离,默认滑动距离为 defaultDistance
getScrollDistance { defaultDistance -> defaultDistance - 200 }
getScrollViewId { R.id.recycler_view }
}
.addPanelHeightMeasurer { //可选 用于设置未获取输入法高度前面板的高度,如果不设置则默认以框架内高度为主
synchronizeKeyboardHeight { false } // 设置面板高度是否和键盘高度同步
getTargetPanelDefaultHeight { DisplayUtils.dip2px(this@DefaultHeightPanelActivity,400f)}
getPanelTriggerId { R.id.add_btn }
}
.contentScrollOutsideEnable(true) //可选,默认为true
.logTrack(true) //可选,默认false,是否开启log信息输出
.build(true) //可选,默认false,是否默认打开输入法
}
}
@Override
public void onBackPressed() {
//用户按下返回键的时候,如果显示面板,则需要隐藏
if (mHelper != null && mHelper.hookSystemBackForHindPanel()) {
return;
}
super.onBackPressed();
}
编写该项目只是希望能提高日常开发的效率,专注于处理业务。如果更好的做法或者意见建议,欢迎写信到 [email protected] 。 也可以微信“lym_llllll” 添加微信进群反馈。
如果框架对你有帮助,可安利给身边的伙伴,每一个 star 都是对框架付出的肯定。