前言
众所周知,google是没有为android提供官方的API来监听软键盘的弹出与关闭的,通俗的做法都是监听Activity这个window的布局变化来判断是否弹出/关闭软键盘
代码实现
需要说明的是,这儿并不使用ViewTreeObserver.OnGlobalLayoutListener来实现对布局的监听,而是会开一个子线程定时检查,因为在实际生产中发现,onGlobalLayout()的刷新时间是不确定的,跟布局的复杂程度有关,有的时候可能要2/3秒才会回调一次,靠这个来监听window的变化来判断是否弹出/关闭小键盘都凉成什么样了。
这儿会使用RxJava来开一个子线程,见Android Kotlin 基于RxJava的简单封装,
弄一个looper来不断循坏检查window的变化,发生变化时便会吐出一个事件。
IKeyBoardCallback
interface IKeyBoardCallback {
/**
* 当键盘显示时回调
*/
fun onKeyBoardShow()
/**
* 当键盘隐藏时回调
*/
fun onKeyBoardHidden()
}
GlobalLayoutListenerTask
class GlobalLayoutListenerTask(private val activity: Activity) : SingleTask<Unit>(){
private val iKeyBoardCallbackList = mutableListOf<IKeyBoardCallback>()
private var status = NONE
private val interval = 100L
/** 全屏时的高度 */
private var fullScreenHeight = -1
/** 状态栏高度 */
private var statusBarHeight = -1
override fun onTaskRun() {
while (isRunning){
try {
//获取可视范围
val rect = Rect()
activity.window.decorView.getWindowVisibleDisplayFrame(rect)
//获取屏幕高度
val screenHeight = getFullScreenHeight(activity)
//获取状态栏高度
val statueHeight = getStatueBarHeight(activity)
//获取被遮挡高度(键盘高度)(屏幕高度-状态栏高度-可视范围)
val keyBoardHeight: Int = screenHeight - statusBarHeight - rect.height()
//显示或者隐藏
val isKeyBoardShow = keyBoardHeight >= screenHeight / 3
//当首次或者和之前的状态不一致的时候会回调,反之不回调(用于当状态变化后才回调,防止多次调用)
if (status == NONE || (isKeyBoardShow && status == HIDDEN) || (!isKeyBoardShow && status == SHOW)) {
if (isKeyBoardShow) {
status = SHOW
dispatchKeyBoardShowEvent()
} else {
status = HIDDEN
dispatchKeyBoardHiddenEvent()
}
}
Thread.sleep(interval)
} catch (e: Exception){
e.printStackTrace()
}
}
}
/**
* 用于获取全屏时的整体高度
*
* @return 屏幕高度
*/
private fun getFullScreenHeight(activity: Activity): Int {
if (fullScreenHeight == -1){
val vm = activity.windowManager
fullScreenHeight = vm.defaultDisplay.height
}
return fullScreenHeight
}
/**
* 用于获取状态栏高度
*
* @return 状态栏高度
*/
private fun getStatueBarHeight(activity: Activity): Int {
if (statusBarHeight == -1){
val res = activity.resources
val resId = res.getIdentifier("status_bar_height", "dimen", "android")
statusBarHeight = res.getDimensionPixelSize(resId)
}
return statusBarHeight;
}
/**
* 添加监听回调
*
* @param callback 监听的回调类
*/
fun addCallBack(callback: Any?){
if (callback is IKeyBoardCallback) iKeyBoardCallbackList.add(callback)
}
/**
* 移除监听回调
*
* @param callback 监听的回调类
*/
fun removeCallback(callback: Any?){
if (callback is IKeyBoardCallback) iKeyBoardCallbackList.remove(callback)
}
/**
* 分发隐藏事件
*/
private fun dispatchKeyBoardHiddenEvent(){
for (callback in iKeyBoardCallbackList){
callback.onKeyBoardHidden()
}
}
/**
* 分发显示事件
*/
private fun dispatchKeyBoardShowEvent(){
for (callback in iKeyBoardCallbackList){
callback.onKeyBoardShow()
}
}
/**
* 判断是不是没有监听回调
*
* @return true:空 false:不空
*/
fun isEmpty(): Boolean = iKeyBoardCallbackList.isEmpty()
companion object {
private const val NONE = 0;
private const val SHOW = 1;
private const val HIDDEN = 2;
}
}
KeyBoardEventBus
object KeyBoardEventBus {
private val taskCache: Hashtable<Any,GlobalLayoutListenerTask> = Hashtable()
/**
* 用于注册键盘监听,此方法适用于 View、Dialog、Fragement、FragementActivity、Activity
*
* @param obj 需要监听的类()
*/
fun register(obj: Any?){
val activity = getActivity(obj)
if (activity == null){
debug("register时获取activity失败!")
return
}
register(activity, obj)
}
/**
* 此方法区别于 {@link #register(Object)} ,之前的方法会限制注册的类型,当前的不会限制类型
*
* @param activity 宿主activity
* @param obj 监听的类
*/
fun register(activity: Activity, obj: Any?){
if (obj == null) {
debug("object为null!")
return
}
var task = taskCache[activity]
if (task == null){
task = GlobalLayoutListenerTask(activity)
}
task.addCallBack(obj)
if (!task.isEmpty()) task.start()
taskCache[activity] = task
}
/**
* 反注册
*
* @param obj 取消监听的类
*/
fun unRegister(obj: Any?){
//获取失败则直接停止,反之进行反注册
val activity = getActivity(obj)
if (activity == null){
debug("unRegister时获取activity失败")
return
}
unRegister(activity, obj)
}
/**
* 反注册
*
* @param activity 宿主activity
* @param obj 监听的类
*/
fun unRegister(activity: Activity, obj: Any?){
if ( obj == null) {
debug("activity或object为null!")
return
}
val task = taskCache[activity] ?: return
task.removeCallback(obj)
if (task.isEmpty()){
task.cancel()
taskCache.remove(task)
}
}
/**
* 获取对应View、Dialog、Fragment、FragmentActivity、Activity
* (如果Object为null或者不是支持的类型则返回null)
*
* @param obj 需要获取的类
* @return 返回对应的activity
*/
private fun getActivity(obj: Any?): Activity?{
if (obj == null) return null
return when(obj){
is View -> obj.context as Activity
is Dialog -> obj.context as Activity
is Fragment -> obj.activity
is FragmentActivity -> obj
is Activity -> obj
else -> null
}
}
/**
* 用于打印信息
*
* @param msg 待打印的内容
*/
private fun debug(msg: String){
Log.e("KeyBoardEventBus",msg)
}
}
用法
postUI见Android Kotlin 代码笔记,全局的UI线程回调函数(基于扩展函数)
用来回调到UI线程弹Toast的
class MainActivity : AppCompatActivity(),IKeyBoardCallback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
KeyBoardEventBus.register(this)
}
override fun onDestroy() {
super.onDestroy()
KeyBoardEventBus.unRegister(this)
}
override fun onKeyBoardShow() {
postUI {
Toast.makeText(this, "键盘显示",Toast.LENGTH_SHORT).show()
}
}
override fun onKeyBoardHidden() {
postUI {
Toast.makeText(this, "键盘隐藏",Toast.LENGTH_SHORT).show()
}
}
}