Android软键盘弹起交互的几种方案,如何做效果最丝滑?

作者:newki

链接:

https://mp.weixin.qq.com/s/D3206Xn0breh9gKeV6y0lg

https://juejin.cn/post/7150453629021847566

本文由作者授权发布。


本文我们会一起复习一下软键盘高度获取的几种方式,布局贴在软键盘上效果的实现与优化。

事情是这样的,有一天我逛PDD的时候,发现这样一个效果:

在搜索页面中,如果软件弹起了就会有一个语音搜索的布局,当我们隐藏软键盘之后就隐藏这个布局。

然后我又看了一下TB的搜索页面,都是类似的效果,但是我发现他们的效果都有优化的空间。

他们的做法是获取到软键盘弹起之后的高度,然后把布局设置到软键盘上面,这个大家都会,但是布局在添加到软键盘之后,软键盘才会慢慢的做一个平移动画展示到指定的位置,如果把动画效果放慢就可以很明显的看到效果。

能不能让我们的布局附着在软键盘上面,随着软键盘的平移动画而动呢?这样的话效果是不是会更流畅一点?

下面我们举例说明一下之前的老方法直接获取到软键盘高度,把布局放上去的做法,和随着软键盘一起动的做法,这两种做法的区别。

1

获取软键盘高度-方式一

要说获取软键盘的高度,那么肯定离不开 getViewTreeObserver().addOnGlobalLayoutListener 的方式。

只是使用起来又分不同的做法,最简单的是拿到Activity的ContentView,设置

contentView.getViewTreeObserver()                 .addOnGlobalLayoutListener(onGlobalLayoutListener);

然后在监听内部再通过 decorView.getWindowVisibleDisplayFrame来获取显示的Rect,在通过 decorView.getBottom() - outRect.bottom的方式来获取高度。

完整示例如下:

publicfinalclassKeyboard1Utils{

publicstaticintsDecorViewInvisibleHeightPre;

privatestaticViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener;

privateKeyboard1Utils(){

}

privatestaticintsDecorViewDelta =0;

privatestaticintgetDecorViewInvisibleHeight(finalActivity activity){

finalView decorView = activity.getWindow().getDecorView();

if(decorView ==null)returnsDecorViewInvisibleHeightPre;

finalRect outRect =newRect();

decorView.getWindowVisibleDisplayFrame(outRect);

intdelta = Math.abs(decorView.getBottom() - outRect.bottom);

if(delta <= getNavBarHeight()) {

sDecorViewDelta = delta;

return0;

}

returndelta - sDecorViewDelta;

}

publicstaticvoidregisterKeyboardHeightListener(finalActivity activity,finalKeyboardHeightListener listener){

finalintflags = activity.getWindow().getAttributes().flags;

if((flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) !=0) {

activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);

}

finalFrameLayout contentView = activity.findViewById(android.R.id.content);

sDecorViewInvisibleHeightPre = getDecorViewInvisibleHeight(activity);

ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener =newViewTreeObserver.OnGlobalLayoutListener() {

@Override

publicvoidonGlobalLayout(){

intheight = getDecorViewInvisibleHeight(activity);

if(sDecorViewInvisibleHeightPre != height) {

listener.onKeyboardHeightChanged(height);

sDecorViewInvisibleHeightPre = height;

}

}

};

contentView.getViewTreeObserver()

.addOnGlobalLayoutListener(onGlobalLayoutListener);

}

publicstaticvoidunregisterKeyboardHeightListener(Activity activity){

onGlobalLayoutListener =null;

View contentView = activity.getWindow().getDecorView().findViewById(android.R.id.content);

if(contentView ==null)return;

contentView.getViewTreeObserver().removeGlobalOnLayoutListener(onGlobalLayoutListener);

}

privatestaticintgetNavBarHeight(){

Resources res = Resources.getSystem();

intresourceId = res.getIdentifier("navigation_bar_height","dimen","android");

if(resourceId !=0) {

returnres.getDimensionPixelSize(resourceId);

}else{

return0;

}

}

publicinterfaceKeyboardHeightListener{

voidonKeyboardHeightChanged(intheight);

}

}

使用:

overridefuninit(){

Keyboard1Utils.registerKeyboardHeightListener(this) {

YYLogUtils.w("当前的软键盘高度:$it")

}

}

Log如下:

需要注意的是方法内部获取导航栏的方法是过时的,部分手机会有问题,但是并没有用它做计算,只是用于一个Flag,终归还是能用,经过我的测试也并不会影响效果。

2

获取软键盘高度-方式二

获取软键盘高度的第二种方式也是使用 getViewTreeObserver().addOnGlobalLayoutListener 的方式,不过不同的是,它是在Activity添加了一个PopupWindow,然后让软键盘弹起的时候,计算PopopWindow移动了多少范围,从而计算软键盘的高度。

这个是网上用的比较多的一种开源方案,别的不说这个思路就是清奇,真是和尚的房子-秒啊。

它创建一个看不见的弹窗,即宽为0,高为全屏,并为弹窗设置全局布局监听器。

当布局有变化,比如有输入法弹窗出现或消失时, 监听器回调函数就会被调用。

而其中的关键就是当输入法弹出时, 它会把之前我们创建的那个看不见的弹窗往上挤, 这样我们创建的那个弹窗的位置就变化了,只要获取它底部高度的变化值就可以间接的获取输入法的高度了。

这里我对源码做了一点修改:

publicclassKeyboardHeightUtilsextendsPopupWindow{

privateKeyboardHeightListener mListener;

privateView popupView;

privateView parentView;

privateActivity activity;

publicKeyboardHeightUtils(Activity activity){

super(activity);

this.activity = activity;

LayoutInflater inflator = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);

this.popupView = inflator.inflate(R.layout.keyboard_popup_window,null,false);

setContentView(popupView);

setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);

setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);

parentView = activity.findViewById(android.R.id.content);

setWidth(0);

setHeight(WindowManager.LayoutParams.MATCH_PARENT);

popupView.getViewTreeObserver().addOnGlobalLayoutListener(newViewTreeObserver.OnGlobalLayoutListener() {

@Override

publicvoidonGlobalLayout(){

if(popupView !=null) {

handleOnGlobalLayout();

}

}

});

}

publicvoidstart(){

parentView.addOnAttachStateChangeListener(newView.OnAttachStateChangeListener() {

@Override

publicvoidonViewAttachedToWindow(View view){

if(!isShowing() && parentView.getWindowToken() !=null) {

setBackgroundDrawable(newColorDrawable(0));

showAtLocation(parentView, Gravity.NO_GRAVITY,0,0);

}

}

@Override

publicvoidonViewDetachedFromWindow(View view){

}

});

}

publicvoidclose(){

this.mListener =null;

dismiss();

}

publicvoidregisterKeyboardHeightListener(KeyboardHeightListener listener){

this.mListener = listener;

}

privatevoidhandleOnGlobalLayout(){

Point screenSize =newPoint();

activity.getWindowManager().getDefaultDisplay().getSize(screenSize);

Rect rect =newRect();

popupView.getWindowVisibleDisplayFrame(rect);

intkeyboardHeight = screenSize.y - rect.bottom;

notifyKeyboardHeightChanged(keyboardHeight);

}

privatevoidnotifyKeyboardHeightChanged(intheight){

if(mListener !=null) {

mListener.onKeyboardHeightChanged(height);

}

}

publicinterfaceKeyboardHeightListener{

voidonKeyboardHeightChanged(intheight);

}

}

使用的方式:

overridefuninit(){

keyboardHeightUtils = KeyboardHeightUtils(this)

keyboardHeightUtils.registerKeyboardHeightListener {

YYLogUtils.w("第二种方式:当前的软键盘高度:$it")

}

keyboardHeightUtils.start()

}

overridefunonDestroy(){

super.onDestroy()

Keyboard1Utils.unregisterKeyboardHeightListener(this)

keyboardHeightUtils.close();

}

Log如下:

和第一种方案有异曲同工之妙,都是一个方法,但是思路有所不同,但是这种方法也有一个坑点,就是需要计算状态栏的高度。可以看到第二种方案和第一种方案有一个状态栏高度的偏差,大家记得处理即可。

3

获取软键盘高度-方式三

之前的文章我们讲过 WindowInsets 的方案,这里我们进一步说一下使用 WindowInsets 获取软键盘高度的坑点。

如果能直接使用兼容方案,那肯定是完美的:

ViewCompat.setWindowInsetsAnimationCallback(window.decorView,object: WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {

overridefunonProgress(insets:WindowInsetsCompat, runningAnimations:MutableList): WindowInsetsCompat {

valisVisible = insets.isVisible(WindowInsetsCompat.Type.ime())

valkeyboardHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

//当前是否展示

YYLogUtils.w("isVisible =$isVisible")

//当前的高度进度回调

YYLogUtils.w("keyboardHeight =$keyboardHeight")

returninsets

}

})

ViewCompat.getWindowInsetsController(findViewById(android.R.id.content))?.apply {

show(WindowInsetsCompat.Type.ime())

}

可惜想法很好,实际上也只有在Android R 以上才好用,低版本要么就只触发一次,要么就干脆不触发。兼容性的方案也有兼容性问题!

具体可以参考我之前的文章,按照我们之前的说法,我们需要在Android11上使用动画监听的方案,而Android11一下使用 setOnApplyWindowInsetsListener 的方式来获取。

https://juejin.cn/post/7149330784891961381

代码大概如下

funaddKeyBordHeightChangeCallBack(view:View, onAction: (height:Int)->Unit) {

varposBottom:Int

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

valcb =object: WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {

overridefunonProgress(

insets:WindowInsets,

animations:MutableList

): WindowInsets {

posBottom = insets.getInsets(WindowInsets.Type.ime()).bottom +

insets.getInsets(WindowInsets.Type.systemBars()).bottom

onAction.invoke(posBottom)

returninsets

}

}

view.setWindowInsetsAnimationCallback(cb)

}else{

ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->

posBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom +

insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom

onAction.invoke(posBottom)

insets

}

}

}

但是实测之后发现,就算是兼容版本的 setOnApplyWindowInsetsListener 方法,获取状态栏和导航栏没有问题,但是当软键盘弹起和收起的时候并不会再次回调,也就是部分设备和版本只能调用一次,再次弹软键盘的时候就不触发了。

这... 又是一个坑。

所以我们如果想兼容版本的话,那没办法了,只能出绝招了,我们就把Android11以下的机型使用 getViewTreeObserver().addOnGlobalLayoutListener 的方式,而Android11以上的我们使用 WindowInsets 的方案。

具体的兼容方案如下:

publicfinalclassKeyboard4Utils{

publicstaticintsDecorViewInvisibleHeightPre;

privatestaticViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener;

privateKeyboard4Utils(){

}

privatestaticintsDecorViewDelta =0;

privatestaticintgetDecorViewInvisibleHeight(finalActivity activity){

finalView decorView = activity.getWindow().getDecorView();

if(decorView ==null)returnsDecorViewInvisibleHeightPre;

finalRect outRect =newRect();

decorView.getWindowVisibleDisplayFrame(outRect);

intdelta = Math.abs(decorView.getBottom() - outRect.bottom);

if(delta <= getNavBarHeight()) {

sDecorViewDelta = delta;

return0;

}

returndelta - sDecorViewDelta;

}

publicstaticvoidregisterKeyboardHeightListener(finalActivity activity,finalKeyboardHeightListener listener){

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

invokeAbove31(activity, listener);

}else{

invokeBelow31(activity, listener);

}

}

@RequiresApi(api = Build.VERSION_CODES.R)

privatestaticvoidinvokeAbove31(Activity activity, KeyboardHeightListener listener){

activity.getWindow().getDecorView().setWindowInsetsAnimationCallback(newWindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {

@NonNull

@Override

publicWindowInsetsonProgress(@NonNull WindowInsets windowInsets, @NonNull List<WindowInsetsAnimation> list){

intheight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom;

listener.onKeyboardHeightChanged(height);

returnwindowInsets;

}

});

}

privatestaticvoidinvokeBelow31(Activity activity, KeyboardHeightListener listener){

finalintflags = activity.getWindow().getAttributes().flags;

if((flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) !=0) {

activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);

}

finalFrameLayout contentView = activity.findViewById(android.R.id.content);

sDecorViewInvisibleHeightPre = getDecorViewInvisibleHeight(activity);

onGlobalLayoutListener =newViewTreeObserver.OnGlobalLayoutListener() {

@Override

publicvoidonGlobalLayout(){

intheight = getDecorViewInvisibleHeight(activity);

if(sDecorViewInvisibleHeightPre != height) {

listener.onKeyboardHeightChanged(height);

sDecorViewInvisibleHeightPre = height;

}

}

};

contentView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);

}

publicstaticvoidunregisterKeyboardHeightListener(Activity activity){

onGlobalLayoutListener =null;

View contentView = activity.getWindow().getDecorView().findViewById(android.R.id.content);

if(contentView ==null)return;

contentView.getViewTreeObserver().removeGlobalOnLayoutListener(onGlobalLayoutListener);

}

privatestaticintgetNavBarHeight(){

Resources res = Resources.getSystem();

intresourceId = res.getIdentifier("navigation_bar_height","dimen","android");

if(resourceId !=0) {

returnres.getDimensionPixelSize(resourceId);

}else{

return0;

}

}

publicinterfaceKeyboardHeightListener{

voidonKeyboardHeightChanged(intheight);

}

}

运行的Log如下:

通过这样的方式我们就能实现在 Android R 以上的设备可以有当前的软键盘高度回调,而低版本的会直接回调当前的软键盘需要展示的直接高度。

4

实现布局悬停在软键盘上面

做好了软键盘的高度计算之后,我们就能实现对应的布局了,这里我们以非滚动的固定布局为例子。

我们在底部加入一个ImageView,当软键盘弹起的时候我们显示到软键盘上面,弹出软键盘试试!

哎?怎么没效果??别慌,还没开始呢!下面开始上方案。

这里我们使用方案一来看看效果:

Keyboard1Utils.registerKeyboardHeightListener(this) {

YYLogUtils.w("当前的软键盘高度:$it")

updateVoiceIcon(it)

}

//更新语音图标的位置

privatefunupdateVoiceIcon(height:Int){

mIvVoice.updateLayoutParams {

bottomMargin = height

}

}

我们简单的做一个增加间距的属性。效果如下:

嗯,就是PDD和TB的应用效果了,那之前我们说的随着软键盘的动画而动画的那种效果呢?

其实就是使用第三种方案,不过只有在Android11以上才能生效,其实目前Android11的占有率还可以。

我们使用方案三来试试:

Keyboard3Utils.registerKeyboardHeightListener(this) {

YYLogUtils.w("第三种方式:当前的软键盘高度:$it")

updateVoiceIcon(it)

}

//更新语音图标的位置

privatefunupdateVoiceIcon(height:Int){

mIvVoice.updateLayoutParams {

bottomMargin = height

}

}

效果三的运行效果如下:

这么看能看出效果一和效果三之间的区别吗,沿着软键盘做的位移,由于我是手机录屏MP4转码GIF,所以是渣渣画质,实际效果比GIF要流畅。

就一个字,丝滑!

5

总结

本文的示例都是基于固定布局下的一些软键盘的操作,而如果是ScrollView类似的一些滚动布局下,那么又是另外一种做法,这里没有做对比。由于篇幅原因,后期可能会单独出各种布局下软键盘的与EidtText的位置相关设置。

其实这种把布局贴在软键盘上面的做法,其实在应用开发中还是相对常见的,比如把输入框的Dialog贴在软键盘上面,比如语言搜索的布局放在软键盘上面等等。

对这样的方案来说,其实我们可以尽量的优化一下展示的方式,高版本的手机会更加的丝滑,总的来说使用第三种方案还是不错的,兼容性还可以。

本文用到的一些测试机型为5.0 6.0 7.0 12这些机型,由于时间精力等原因并没有覆盖全版本和机型,如果大家有其他的兼容性问题也能评论区交流一下。如果有其他或更好的方案也可以评论区交流哦。

好了,本文的全部代码与Demo都已经开源。有兴趣可以看这里。项目会持续更新,大家可以关注一下。

https://gitee.com/newki123456/Kotlin-Room

如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。

最后推荐一下我做的网站,玩Android:wanandroid.com,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容