标题没想好叫啥,为了更多人可以看到,所以多加了点关键字~
解决全屏Activity的键盘遮挡输入框
Fullscreen遮挡Edittext
全屏输入框bug
AndroidBug5497Workaround
沉浸式输入框
关于AndroidBug5497Workaround的兼容性问题
华为全屏输入框(PS:为什么有人说华为有这个问题?有虚拟导航栏的都有这个问题好伐。。。)
全屏无法弹出输入法
全屏adjustResize不执行
SoftInputMode无效
安卓键盘挡住输入框
好了,上面是一堆废话,只是为了让更多人可以看到,毕竟老夫找解决方案的时候找了半天也没完美的解决。
最近搞个项目,设计比较奇葩,沉浸式也就罢了,状态栏和Toolbar的背景不是纯色的,是渐变色的,如图:
这尼玛就有点费劲了,自从接手这个方案,然后各种坑扑面而来应接不暇。
沉浸式实现倒是不复杂,但是后续有哪些坑,得慢慢发现。
第一个就是安卓版本问题,我的项目最低支持4.4,所以往下的兼容性就没考虑。5.x-6.x(忘了几点几了)状态栏是可以完全透明的,就是上图的样式,但是再高的,就会是半透明,我勒个去,这哪行啊,作为强迫症患者,显然不允许这种情况发生啊。于是乎,尝试了几个库,最后整合了几个开源库的代码,总算处理好了半透明的问题,但是据说会引发其他bug,诸如页面动画失效之类的,由于没遇到,等遇到再填坑吧。
背景介绍完了,然后接下来就是今天的大bug了,为此差点放弃了沉浸式设计。
聊天界面,如下:
点击输入框之后却发现只有键盘弹上来了,而输入框被键盘挡住了,我去,显然不行啊。看起来就像是adjustResize没有发挥他的效果。
咋整捏,解决吧,到处找方案,才发现此问题由来已久,而且貌似谷歌官方不当回事,一直没有官方的解决办法(如果有,欢迎告知)后来才了解到AndroidBug5497Workaround,但是看到有人说在华为的机器上会有个黑条,这种不完美的方案就被我否定了,当时没仔细研究他的代码,不想重复造轮子,于是找到这篇博文:https://blog.csdn.net/smileiam/article/details/69055963 里面的方法5,据说比较完美,于是直接拿来用,跑起来发现,键盘虽然推起来了,但是键盘跟输入框之间有个明显的距离啊,目测等于状态栏高度。
想到其他人评论说的,华为机器上有黑条,莫非就是这个条?要是这个条真是状态栏高度,倒也好解决,我的担心的是另一个bug,三星Note8的虚拟导航栏,可以双击这个点来隐藏,正常情况下,隐藏之后,activity里的高度会变,内容会填充到虚拟导航栏的位置:
然后界面应该变成这样子:
测试一下,果然是不如老夫所愿,虚拟按键隐藏了,而输入框稳稳的停在了那个位置,不卑不亢:
开始排查吧,到底为什么会这样,首先确定是不是沉浸式UI造成这个恶果,不调用这个修复输入法弹出的帮助类,app是正常的,跟随虚拟导航栏隐藏与显示而填充界面或者缩短,调用之后,界面就像是固定了。诶?固定了?莫非是大小被固定了?虽然不想重复造轮子,但是问题得解决啊,于是乎开始学习前辈们的源码:
/**
* 解决键盘档住输入框
* Created by SmileXie on 2017/4/3.
*/public class SoftHideKeyBoardUtil {
public static void assistActivity (Activity activity) {
new SoftHideKeyBoardUtil(activity);
}
private View mChildOfContent;
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
//为适应华为小米等手机键盘上方出现黑条或不适配
private int contentHeight;//获取setContentView本来view的高度
private boolean isfirst = true;//只用获取一次
private int statusBarHeight;//状态栏高度
private SoftHideKeyBoardUtil(Activity activity) {
//1、找到Activity的最外层布局控件,它其实是一个DecorView,它所用的控件就是FrameLayout
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
//2、获取到setContentView放进去的View
mChildOfContent = content.getChildAt(0);
//3、给Activity的xml布局设置View树监听,当布局有变化,如键盘弹出或收起时,都会回调此监听
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
//4、软键盘弹起会使GlobalLayout发生变化
public void onGlobalLayout() {
if (isfirst) {
contentHeight = mChildOfContent.getHeight();//兼容华为等机型
isfirst = false;
}
//5、当前布局发生变化时,对Activity的xml布局进行重绘
possiblyResizeChildOfContent();
}
});
//6、获取到Activity的xml布局的放置参数
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
}// 获取界面可用高度,如果软键盘弹起后,Activity的xml布局可用高度需要减去键盘高度
private void possiblyResizeChildOfContent() {
//1、获取当前界面可用高度,键盘弹起后,当前界面可用布局会减少键盘的高度
int usableHeightNow = computeUsableHeight();
//2、如果当前可用高度和原始值不一样
if (usableHeightNow != usableHeightPrevious) {
//3、获取Activity中xml中布局在当前界面显示的高度
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
//4、Activity中xml布局的高度-当前可用高度
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
//5、高度差大于屏幕1/4时,说明键盘弹出
if (heightDifference > (usableHeightSansKeyboard/4)) {
// 6、键盘弹出了,Activity的xml布局高度应当减去键盘高度
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference + statusBarHeight;
} else {
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
}
} else {
frameLayoutParams.height = contentHeight;
}
//7、 重绘Activity的xml布局
mChildOfContent.requestLayout();
usableHeightPrevious = usableHeightNow;
}
}
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
// 全屏模式下:直接返回r.bottom,r.top其实是状态栏的高度
return (r.bottom - r.top);
}
}
大佬的注释很清楚。
首先发现 statusBarHeight 没有赋值啊,搞什么哇?注释了半天把个变量给拉下了??严重怀疑那个白块就是这个的问题。
然后继续看,搞明白了原理,原来就是当高度改变的时候,超过阈值就认为是键盘弹出,阈值有设置为四分之一的,有设置为200px的,不管多少,总感觉不靠谱啊,可是又没其他办法,仔细想想也想不出来还有什么情况会导致高度改变(比如手机突然换了块屏幕?)。最终无可奈何的认同了这种做法。
也不知道是不是大家都不爱动脑,恢复高度的时候,这里竟然传入了第一次获取到的高度,那问题就来了,当我隐藏虚拟导航栏的时候,高度明明变了,为啥还要传第一次获取到的高度呢?不是应该交给系统控制的么?所以这里:
//5、高度差大于屏幕1/4时,说明键盘弹出
if (heightDifference > (usableHeightSansKeyboard/4)) {
xxxxxxx......
} else {
frameLayoutParams.height = contentHeight;
}
明显是罪魁祸首,改成:
} else { mFrameLayoutParams.height = FrameLayout.LayoutParams.MATCH_PARENT; }
那些啥第一获取的值之类的变量,不想关的都去掉。跑一下试试,果然,再次隐藏显示虚拟导航栏,bug就都解决了。
当然,上面的还不够优雅,打log会发现,onGlobalLayout被多次调用,所以去掉不需要的调用:
if (mFrameLayoutParams.height != height) {//不必要的更新就不要了 mFrameLayoutParams.height = height; mContentView.requestLayout();//触发布局更新 }
到这里,基本上遇到的问题就解决了。
但是,我的Note8是安卓8.0,支持分屏的,虽然没咋用过,但是正好可以试试,会不会导致异常情况呢?
果然,又出问题了,遮挡了一半,目测又是个状态栏高度(这个目测真厉害...),先观察了一下,其他app,注意这个位置:
比正常情况要高(比其他app要高),说明已经有了个状态栏高度了,其他app在分屏情况下,没有这么高,于是乎心里大概有谱了,我的app不知道系统为什么把状态栏也给我算在内了(难道是fitsSystemWindows的锅?),而我又多加了个状态栏高度,所以导致这种情况。因此首先想到,在分屏的情况下,就不加这个状态栏高度,代码改成:
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference ;// + statusBarHeight;
跑一下子试试呗~嗯,效果8错。
交换一下分屏窗口的上下位置,拉长一下app窗口长度,另一个窗口自动缩短,然后再试一下,哎哟我去,又tm不行了??
可以看到,输入法遮挡住下面小一点的APP窗口之后又遮挡住了我的app的下边一部分,这部分刚好包括我的输入框。。。。
分析一下,就到了之前说的,变动距离超过阈值就认为是键盘弹出,显然,这种情况移动的距离太小太小,达不到阈值,所以回调里就给忽略了!没办法,只好改成当处于分屏的情况下,移动距离只要大于0就认为是键盘弹出,暂时没想到会不会有其他情况。
修改完成之后,一切顺利,横竖屏都没有问题。
最开始的时候,我只在竖屏里处理这个情况,但是发现,分屏的时候,屏幕方向orientation竟然等于ORIENTATION_LANDSCAPE,而不是ORIENTATION_PORTRAIT,不知道这是什么情况。。。于是不再用orientation作为依据。好在横屏时,一般输入法都会满屏,也就看不到遮挡不遮挡输入框了。只是不知道会不会有横屏输入法不是满屏的情况~~~
代码都修改完了,才发现,原来大佬这段代码的前身就是AndroidBug5497Workaround的代码,只不过是大佬处理华为等手机的时候多添加了一段代码。所以本次就算是对他的一个增强完善吧。
原理都已经讲完了,我遇到的实际问题也解决了,但是总感觉这样并不是完美的方案。如果你有更好的经过验证的想法,可以告诉我哟~
如果下面的源码对你有帮助或者是没有解决你的问题,可以留言分享一下,并不是我会帮你解决,只是为了给更多人参考,也是为了他人不误入歧途,吼吼~
以下是工具类的源码:
一些不用的方法是备用的,怕遇到其他情况可以快速修改。自己随意删~
/**
* 解决全屏Activity的键盘档住输入框
* 来自:https://blog.csdn.net/passerby_b/article/details/82686662
* 注意:
* 1.要在setContentView之后调用 assistActivity(activity)!
* 2.要是横屏输入法不是满屏的,就需要自己适配了!
* 3.自测没有发现问题,但无法100%保证兼容性~
* 4.分屏模式下的处理,不知道会不会有其他问题,如果不是刚需,建议还是通过setSoftInputMode尝试调整~~~
*
* 更新 Cooper 2018-9-13 13:27:59
* 1.解决虚拟导航栏隐藏显示布局不自动适配的问题(三星Note8 8.0实测横屏竖屏都没问题,Vivo没有虚拟按键的机器6.0测试没有问题)
* 2.解决分屏模式下不适配的问题(三星Note8 8.0实测横屏竖屏都没问题)
* 3.优化代码
*
* 参考:https://blog.csdn.net/smileiam/article/details/69055963
* 参考:https://blog.csdn.net/auccy/article/details/80632429
* 参考:https://github.com/yy1300326388/AndroidBarUtils/blob/master/app/src/main/java/cn/zsl/androidbarutils/utils/AndroidBarUtils.java
* 其实最初的原版就是 AndroidBug5497Workaround ,但是原版考虑的不够全面,尤其是虚拟导航栏的问题,没有考虑进去
* 参考:https://www.jianshu.com/p/a95a1b84da11
*/
public class SoftKeyboardFixerForFullscreen {
public static void assistActivity(Activity activity) {
new SoftKeyboardFixerForFullscreen(activity);
}
private View mContentView;//我们设置的contentView
private FrameLayout.LayoutParams mFrameLayoutParams;//我们设置的contentView的layoutParams
// private boolean isNavigationShowing = false;//没有用到这个
// private boolean isFullscreenMode = false;//没有用到这个
// private int barNavigationHeight = 0;//虚拟导航栏高度,没用
// private int barNavigationWidth = 0;//虚拟导航栏宽度,没用
private int barStatusHeight = 0;//状态栏高度
private int lastUsableHeight = 0;//上一次的可用高度
private int lastUsableWidth = 0;//上一次的可用宽度
private SoftKeyboardFixerForFullscreen(final Activity activity) {
//region 本来是想通过这个监听虚拟按键,结果发现这个回调比布局回调要晚,所以不用了。放在这里是为了给以后提供一些思路。
// //1.为DecorView添加系统组件的可见变更事件
// View decorView = activity.getWindow().getDecorView();
// isNavigationShowing = ((decorView.getSystemUiVisibility() & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0);
// isFullscreenMode = ((decorView.getSystemUiVisibility() & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0);//api 16以上
// decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {//参考:https://blog.csdn.net/auccy/article/details/80632429
// @Override
// public void onSystemUiVisibilityChange(int visibility) {
// if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
// isNavigationShowing = true;
// } else {
// isNavigationShowing = false;
// }
// if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
// isFullscreenMode = true;
// } else {
// isFullscreenMode = true;
// }
// }
// });
//endregion
//1.获取 状态栏 高度,获取 导航栏 高度、宽度(横屏用到的,可是横屏在手机上输入法会满屏,不知道不满屏的情况,所以不处理了,要是你遇到了,自行按照横屏的方式解决吧)
barStatusHeight = getStatusBarHeight(activity);
//barNavigationHeight = getNavigationBarHeight(activity);
//barNavigationWidth = getNavigationBarWidth(activity);
//2.找到Activity的最外层布局控件,它其实是一个DecorView,它所用的控件就是FrameLayout
final FrameLayout content = activity.findViewById(android.R.id.content);
//3.获取到setContentView放进去的View
mContentView = content.getChildAt(0);
//4.拿到我们设置的View的布局参数,主要是调整该参数来实现软键盘弹出上移
mFrameLayoutParams = (FrameLayout.LayoutParams) mContentView.getLayoutParams();
//5.给我们设置的View添加布局变动的监听,来实现布局动作(虚拟导航栏的弹出收起也会触发该监听!)
mContentView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {//软键盘弹出、系统导航栏隐藏显示均会触发这里
int heightRoot = content.getRootView().getHeight();//包含虚拟按键的高度(如果有的话)
int heightDecor = content.getHeight();//不含虚拟按键的高度,貌似不包含状态栏高度
int usableHeight = computeUsableHeight();//我们setContentView设置的view的可用高度
if (usableHeight != lastUsableHeight) {
lastUsableHeight = usableHeight;//防止重复变动
int heightDifference = heightDecor - usableHeight;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode()) {//如果是分屏模式
if (heightDifference > 0) {//分屏模式,只要变动了就人为弹出键盘,因为分屏可能该Activity是在手机屏幕的上方,弹出输入法只是遮盖了一丁点~如果不合适,需要你自己适配了!
setHeight(heightDecor - heightDifference); //这里不能加状态栏高度哟~
} else {
setHeight(FrameLayout.LayoutParams.MATCH_PARENT);//还原默认高度,不能用计算的值,因为虚拟导航栏显示或者隐藏的时候也会改变高度
}
} else {
if (heightDifference > (heightDecor / 4)) {//高度变动超过decor的四分之一则认为是软键盘弹出事件,为什么不用屏幕高度呢?开始以为这样在分屏模式下也可以监听,但是实测不行。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
setHeight(heightDecor - heightDifference + barStatusHeight);//这里为什么要添加状态栏高度?
} else {
setHeight(heightDecor - heightDifference);//这里不添加状态栏高度?不懂为什么,原版如此,就先这样吧。遇到再说~
}
} else {
setHeight(FrameLayout.LayoutParams.MATCH_PARENT);//还原默认高度,不能用计算的值,因为虚拟导航栏显示或者隐藏的时候也会改变高度
}
}
}
}
});
}
private void setHeight(int height) {
if (mFrameLayoutParams.height != height) {//不必要的更新就不要了
mFrameLayoutParams.height = height;
mContentView.requestLayout();//触发布局更新
}
}
private int computeUsableHeight() {
Rect r = new Rect();
mContentView.getWindowVisibleDisplayFrame(r);
// 全屏模式下:直接返回r.bottom,r.top其实是状态栏的高度
return (r.bottom - r.top);
}
private int computeUsableWidth() {
Rect r = new Rect();
mContentView.getWindowVisibleDisplayFrame(r);
// 全屏模式下:直接返回r.bottom,r.top其实是状态栏的高度//横屏就是宽度
return (r.right - r.left);
}
//下面相关代码来自:https://github.com/yy1300326388/AndroidBarUtils/blob/master/app/src/main/java/cn/zsl/androidbarutils/utils/AndroidBarUtils.java
//完整代码,全屏时有问题。
private static final String STATUS_BAR_HEIGHT_RES_NAME = "status_bar_height";
private static final String NAV_BAR_HEIGHT_RES_NAME = "navigation_bar_height";
private static final String NAV_BAR_WIDTH_RES_NAME = "navigation_bar_width";
/**
* 获取状态栏高度
*
* @param context context
* @return 状态栏高度
*/
private static int getStatusBarHeight(Activity context) {
// 获得状态栏高度
return getBarHeight(context, STATUS_BAR_HEIGHT_RES_NAME);
}
/**
* 获取导航栏高度
*
* @param activity activity
* @return 导航栏高度
*/
private static int getNavigationBarHeight(Activity activity) {
if (hasNavBar(activity)) {
// 获得导航栏高度
return getBarHeight(activity, NAV_BAR_HEIGHT_RES_NAME);
} else {
return 0;
}
}
/**
* 获取横屏状态下导航栏的宽度
*
* @param activity activity
* @return 导航栏的宽度
*/
private static int getNavigationBarWidth(Activity activity) {
if (hasNavBar(activity)) {
// 获得导航栏高度
return getBarHeight(activity, NAV_BAR_WIDTH_RES_NAME);
} else {
return 0;
}
}
/**
* 获取Bar高度
*
* @param context context
* @param barName 名称
* @return Bar高度
*/
private static int getBarHeight(Context context, String barName) {
// 获得状态栏高度
int resourceId = context.getResources().getIdentifier(barName, "dimen", "android");
return context.getResources().getDimensionPixelSize(resourceId);
}
/**
* 是否有NavigationBar
*
* @param activity 上下文
* @return 是否有NavigationBar
*/
private static boolean hasNavBar(Activity activity) {
WindowManager windowManager = activity.getWindowManager();
Display d = windowManager.getDefaultDisplay();
DisplayMetrics realDisplayMetrics = new DisplayMetrics();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
d.getRealMetrics(realDisplayMetrics);
}
int realHeight = realDisplayMetrics.heightPixels;
int realWidth = realDisplayMetrics.widthPixels;
DisplayMetrics displayMetrics = new DisplayMetrics();
d.getMetrics(displayMetrics);
int displayHeight = displayMetrics.heightPixels;
int displayWidth = displayMetrics.widthPixels;
return (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0;
}
}
写文章很麻烦,转载注明出处哟。https://blog.csdn.net/passerby_b/article/details/82686662