一、获取软键盘高度之方式一
要说获取软键盘的高度,那么肯定离不开 getViewTreeObserver().addOnGlobalLayoutListener 的方式。
只是使用起来又分不同的做法,最简单的是拿到Activity的ContentView,设置
contentView.getViewTreeObserver() .addOnGlobalLayoutListener(onGlobalLayoutListener);
然后在监听内部再通过 decorView.getWindowVisibleDisplayFrame来获取显示的Rect,在通过 decorView.getBottom() - outRect.bottom的方式来获取高度。
完整示例如下:
public final class Keyboard1Utils {
public static int sDecorViewInvisibleHeightPre;
private static ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener;
private static int mNavHeight;
private Keyboard1Utils() {
}
private static int sDecorViewDelta = 0;
private static int getDecorViewInvisibleHeight(final Activity activity) {
final View decorView = activity.getWindow().getDecorView();
if (decorView == null) return sDecorViewInvisibleHeightPre;
final Rect outRect = new Rect();
decorView.getWindowVisibleDisplayFrame(outRect);
int delta = Math.abs(decorView.getBottom() - outRect.bottom);
if (delta <= mNavHeight) {
sDecorViewDelta = delta;
return 0;
}
return delta - sDecorViewDelta;
}
public static void registerKeyboardHeightListener(final Activity activity, final KeyboardHeightListener listener) {
final int flags = activity.getWindow().getAttributes().flags;
if ((flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) != 0) {
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
}
final FrameLayout contentView = activity.findViewById(android.R.id.content);
sDecorViewInvisibleHeightPre = getDecorViewInvisibleHeight(activity);
onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int height = getDecorViewInvisibleHeight(activity);
if (sDecorViewInvisibleHeightPre != height) {
listener.onKeyboardHeightChanged(height);
sDecorViewInvisibleHeightPre = height;
}
}
};
//获取到导航栏高度之后再添加布局监听
getNavigationBarHeight(activity, new NavigationBarCallback() {
@Override
public void onHeight(int height, boolean hasNav) {
mNavHeight = height;
contentView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
}
});
}
public static void unregisterKeyboardHeightListener(Activity activity) {
onGlobalLayoutListener = null;
View contentView = activity.getWindow().getDecorView().findViewById(android.R.id.content);
if (contentView == null) return;
contentView.getViewTreeObserver().removeGlobalOnLayoutListener(onGlobalLayoutListener);
}
private static int getNavBarHeight() {
Resources res = Resources.getSystem();
int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId != 0) {
return res.getDimensionPixelSize(resourceId);
} else {
return 0;
}
}
public static void getNavigationBarHeight(Activity activity, NavigationBarCallback callback) {
View view = activity.getWindow().getDecorView();
boolean attachedToWindow = view.isAttachedToWindow();
if (attachedToWindow) {
WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(view);
assert windowInsets != null;
int height = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
boolean hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0;
if (height > 0) {
callback.onHeight(height, hasNavigationBar);
} else {
callback.onHeight(getNavBarHeight(), hasNavigationBar);
}
} else {
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);
assert windowInsets != null;
int height = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
boolean hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0;
if (height > 0) {
callback.onHeight(height, hasNavigationBar);
} else {
callback.onHeight(getNavBarHeight(), hasNavigationBar);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
}
});
}
}
public interface KeyboardHeightListener {
void onKeyboardHeightChanged(int height);
}
}
使用:
override fun init() {
Keyboard1Utils.registerKeyboardHeightListener(this) {
YYLogUtils.w("当前的软键盘高度:$it")
}
}
需要注意的是方法内部获取导航栏的方法是过时的,部分手机会有问题,但是并没有用它做计算,只是用于一个Flag,终归还是能用,经过我的测试也并不会影响效果。
二、获取软键盘高度之方式二
获取软键盘高度的第二种方式也是使用 getViewTreeObserver().addOnGlobalLayoutListener 的方式,不过不同的是,它是在Activity添加了一个PopupWindow,然后让软键盘弹起的时候,计算PopopWindow移动了多少范围,从而计算软键盘的高度。
它创建一个看不见的弹窗,即宽为0,高为全屏,并为弹窗设置全局布局监听器。当布局有变化,比如有输入法弹窗出现或消失时, 监听器回调函数就会被调用。而其中的关键就是当输入法弹出时, 它会把之前我们创建的那个看不见的弹窗往上挤, 这样我们创建的那个弹窗的位置就变化了,只要获取它底部高度的变化值就可以间接的获取输入法的高度了。
这里我对源码做了一点修改
public class KeyboardHeightUtils extends PopupWindow {
private KeyboardHeightListener mListener;
private View popupView;
private View parentView;
private Activity activity;
public KeyboardHeightUtils(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(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (popupView != null) {
handleOnGlobalLayout();
}
}
});
}
public void start() {
parentView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
if (!isShowing() && parentView.getWindowToken() != null) {
setBackgroundDrawable(new ColorDrawable(0));
showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0);
}
}
@Override
public void onViewDetachedFromWindow(View view) {
}
});
}
public void close() {
this.mListener = null;
dismiss();
}
public void registerKeyboardHeightListener(KeyboardHeightListener listener) {
this.mListener = listener;
}
private void handleOnGlobalLayout() {
Point screenSize = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(screenSize);
Rect rect = new Rect();
popupView.getWindowVisibleDisplayFrame(rect);
int keyboardHeight = screenSize.y - rect.bottom;
notifyKeyboardHeightChanged(keyboardHeight);
}
private void notifyKeyboardHeightChanged(int height) {
if (mListener != null) {
mListener.onKeyboardHeightChanged(height);
}
}
public interface KeyboardHeightListener {
void onKeyboardHeightChanged(int height);
}
}
使用的方式:
override fun init() {
keyboardHeightUtils = KeyboardHeightUtils(this)
keyboardHeightUtils.registerKeyboardHeightListener {
YYLogUtils.w("第二种方式:当前的软键盘高度:$it")
}
keyboardHeightUtils.start()
}
override fun onDestroy() {
super.onDestroy()
Keyboard1Utils.unregisterKeyboardHeightListener(this)
keyboardHeightUtils.close();
}
和第一种方案有异曲同工之妙,都是一个方法,但是思路有所不同,但是这种方法也有一个坑点,就是需要计算状态栏的高度。可以看到第二种方案和第一种方案有一个状态栏高度的偏差,大家记得处理即可。
三、获取软键盘高度之方式三
如果能直接使用兼容方案,那肯定是完美的:
ViewCompat.setWindowInsetsAnimationCallback(window.decorView, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
val isVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val keyboardHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
//当前是否展示
YYLogUtils.w("isVisible = $isVisible")
//当前的高度进度回调
YYLogUtils.w("keyboardHeight = $keyboardHeight")
return insets
}
})
ViewCompat.getWindowInsetsController(findViewById(android.R.id.content))?.apply {
show(WindowInsetsCompat.Type.ime())
}
可惜想法很好,实际上也只有在Android R 以上才好用,低版本要么就只触发一次,要么就干脆不触发。兼容性的方案也有兼容性问题!
我们需要在Android11上使用动画监听的方案,而Android11一下使用 setOnApplyWindowInsetsListener 的方式来获取。
代码大概如下
fun addKeyBordHeightChangeCallBack(view: View, onAction: (height: Int) -> Unit) {
var posBottom: Int
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val cb = object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
override fun onProgress(
insets: WindowInsets,
animations: MutableList<WindowInsetsAnimation>
): WindowInsets {
posBottom = insets.getInsets(WindowInsets.Type.ime()).bottom +
insets.getInsets(WindowInsets.Type.systemBars()).bottom
onAction.invoke(posBottom)
return insets
}
}
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 方法,获取状态栏和导航栏没有问题,但是当软键盘弹起和收起的时候并不会再次回调,也就是部分设备和版本只能调用一次,再次弹软键盘的时候就不触发了。
如果觉得不保险大家也可以在控件上屏之后再设置监听,onResume中设置监听,这样确保是设置监听成功,在Android11以上的设备,使用兼容方案的监听是可以拿到监听,Android11以下的设备有些也可以拿到监听。
所以我们如果想兼容版本的话,那没办法了,只能出绝招了,我们就把 Android11 以下的机型使用 getViewTreeObserver().addOnGlobalLayoutListener 的方式,而 Android11 以上的我们使用 WindowInsets 的方案,这样就是最为保险的方式。
具体的兼容方案如下:
public final class Keyboard4Utils {
public static int sDecorViewInvisibleHeightPre;
private static ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener;
private static int mNavHeight;
private Keyboard4Utils() {
}
private static int sDecorViewDelta = 0;
private static int getDecorViewInvisibleHeight(final Activity activity) {
final View decorView = activity.getWindow().getDecorView();
if (decorView == null) return sDecorViewInvisibleHeightPre;
final Rect outRect = new Rect();
decorView.getWindowVisibleDisplayFrame(outRect);
int delta = Math.abs(decorView.getBottom() - outRect.bottom);
if (delta <= mNavHeight) {
sDecorViewDelta = delta;
return 0;
}
return delta - sDecorViewDelta;
}
public static void registerKeyboardHeightListener(final Activity activity, final KeyboardHeightListener listener) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
invokeAbove31(activity, listener);
} else {
invokeBelow31(activity, listener);
}
}
@RequiresApi(api = Build.VERSION_CODES.R)
private static void invokeAbove31(Activity activity, KeyboardHeightListener listener) {
activity.getWindow().getDecorView().setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
@NonNull
@Override
public WindowInsets onProgress(@NonNull WindowInsets windowInsets, @NonNull List<WindowInsetsAnimation> list) {
int imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
int navHeight = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
boolean hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0;
listener.onKeyboardHeightChanged(hasNavigationBar ? Math.max(imeHeight - navHeight, 0) : imeHeight);
return windowInsets;
}
});
}
private static void invokeBelow31(Activity activity, KeyboardHeightListener listener) {
final int flags = activity.getWindow().getAttributes().flags;
if ((flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) != 0) {
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
}
final FrameLayout contentView = activity.findViewById(android.R.id.content);
sDecorViewInvisibleHeightPre = getDecorViewInvisibleHeight(activity);
onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int height = getDecorViewInvisibleHeight(activity);
if (sDecorViewInvisibleHeightPre != height) {
listener.onKeyboardHeightChanged(height);
sDecorViewInvisibleHeightPre = height;
}
}
};
//获取到导航栏高度之后再添加布局监听
getNavigationBarHeight(activity, new NavigationBarCallback() {
@Override
public void onHeight(int height, boolean hasNav) {
mNavHeight = height;
contentView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
}
});
}
public static void unregisterKeyboardHeightListener(Activity activity) {
onGlobalLayoutListener = null;
View contentView = activity.getWindow().getDecorView().findViewById(android.R.id.content);
if (contentView == null) return;
contentView.getViewTreeObserver().removeGlobalOnLayoutListener(onGlobalLayoutListener);
}
private static int getNavBarHeight() {
Resources res = Resources.getSystem();
int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId != 0) {
return res.getDimensionPixelSize(resourceId);
} else {
return 0;
}
}
public static void getNavigationBarHeight(Activity activity, NavigationBarCallback callback) {
View view = activity.getWindow().getDecorView();
boolean attachedToWindow = view.isAttachedToWindow();
if (attachedToWindow) {
WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(view);
assert windowInsets != null;
int height = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
boolean hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0;
if (height > 0) {
callback.onHeight(height, hasNavigationBar);
} else {
callback.onHeight(getNavBarHeight(), hasNavigationBar);
}
} else {
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);
assert windowInsets != null;
int height = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
boolean hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0;
if (height > 0) {
callback.onHeight(height, hasNavigationBar);
} else {
callback.onHeight(getNavBarHeight(), hasNavigationBar);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
}
});
}
}
public interface KeyboardHeightListener {
void onKeyboardHeightChanged(int height);
}
}