继续创作,加速生长!这是我参加「日新计划 10 月更文挑战」的第5天,点击查看活动详情
前言
本文咱们会一同复习一下软键盘高度获取的几种办法,布局贴在软键盘上作用的完成与优化。
工作是这样的,有一天我逛PDD的时分,发现这样一个作用,
在查找页面中,假如软件弹起了就会有一个语音查找的布局,当咱们隐藏软键盘之后就隐藏这个布局,
然后我又看了一下TB的查找页面,都是相似的作用,可是我发现他们的作用都有优化的空间。
他们的做法是获取到软键盘弹起之后的高度,然后把布局设置到软键盘上面,这个咱们都会,可是布局在增加到软键盘之后,软键盘才会渐渐的做一个平移动画展现到指定的位置,假如把动画作用怠慢就能够很明显的看到作用。
能不能让咱们的布局附着在软键盘上面,跟着软键盘的平移动画而动呢?这样的话作用是不是会更流通一点?
下面咱们举例说明一下之前的老办法直接获取到软键盘高度,把布局放上去的做法,和跟着软键盘一同动的做法,这两种做法的差异。
一、获取软键盘高度-办法一
要说获取软键盘的高度,那么必定离不开 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")
}
}
Log如下:
需求注意的是办法内部获取导航栏的办法是过时的,部分手时机有问题,可是并没有用它做核算,只是用于一个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();
}
Log如下:
和第一种计划有异曲同工之妙,都是一个办法,可是思路有所不同,可是这种办法也有一个坑点,便是需求核算状态栏的高度。能够看到第二种计划和第一种计划有一个状态栏高度的偏差,咱们记住处理即可。
三、获取软键盘高度-办法三
之前的文章咱们讲过 WindowInsets 的计划,这儿咱们进一步说一下运用 WindowInsets 获取软键盘高度的坑点。
假如能直接运用兼容计划,那必定是完美的:
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 办法,获取状态栏和导航栏没有问题,可是当软键盘弹起和收起的时分并不会再次回调,也便是部分设备和版别只能调用一次,再次弹软键盘的时分就不触发了。
这… 又是一个坑。
2022-10-18 弥补
假如觉得不稳妥咱们也能够在控件上屏之后再设置监听,onResume中设置监听,这样保证是设置监听成功,在Android11以上的设备,运用兼容计划的监听是能够拿到监听,Android11以下的设备有些也能够拿到监听。
因为测验机型有限,这儿只说一下咱们现有的测验机型出现问题的情况:
Oppo A37M – Android5.0 :不触发
Huawei SLA-TL10 – Android7.0 : 只触发一次,导致顶部的图片顶上去就下不来了
所以咱们假如想兼容版别的话,那没办法了,只能出绝招了,咱们就把 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);
}
}
运转的Log如下:
通过这样的办法咱们就能完成在 Android R 以上的设备能够有当时的软键盘高度回调,而低版别的会直接回调当时的软键盘需求展现的直接高度。
记住需求判断是否需求处理导航栏的高度哦,就算是R以上的咱们也需求判断是否需求减去导航栏高度的。
四、完成布局悬停在软键盘上面
做好了软键盘的高度核算之后,咱们就能完成对应的布局了,这儿咱们以非翻滚的固定布局为比方。
咱们在底部加入一个ImageView,当软键盘弹起的时分咱们显现到软键盘上面,弹出软键盘试试!
哎?怎么没作用??别慌,还没开端呢!下面开端上计划。
这儿咱们运用计划一来看看作用:
Keyboard1Utils.registerKeyboardHeightListener(this) {
YYLogUtils.w("当时的软键盘高度:$it")
updateVoiceIcon(it)
}
//更新语音图标的位置
private fun updateVoiceIcon(height: Int) {
mIvVoice.updateLayoutParams<FrameLayout.LayoutParams> {
bottomMargin = height
}
}
咱们简略的做一个增加距离的特点。作用如下:
嗯,便是PDD和TB的运用作用了,那之前咱们说的跟着软键盘的动画而动画的那种作用呢?
其实便是运用第三种计划,不过只要在Android11以上才干生效,其实现在Android11的占有率还能够。接下来咱们换一个手机试试。
没什么作用?是的,我还没换呢,闹个眼子。先发一个作用一的图来做一下比照嘛。
接下来咱们运用计划三来试试:
Keyboard3Utils.registerKeyboardHeightListener(this) {
YYLogUtils.w("第三种办法:当时的软键盘高度:$it")
updateVoiceIcon(it)
}
//更新语音图标的位置
private fun updateVoiceIcon(height: Int) {
mIvVoice.updateLayoutParams<FrameLayout.LayoutParams> {
bottomMargin = height
}
}
作用三的运转作用如下:
这么看能看出作用一和作用三之间的差异吗,沿着软键盘做的位移,因为我是手机录屏MP4转码GIF,所以是渣渣画质,实际作用比GIF要流通。defu纵享丝滑!
总结
本文的示例都是根据固定布局下的一些软键盘的操作,而假如是ScrollView相似的一些翻滚布局下,那么又是另外一种做法,这儿没有做比照。因为篇幅原因,后期可能会单独出各种布局下软键盘的与EidtText的位置相关设置。(文章已出,有爱好能够看看这篇文章【传送门】)
话说回来,其实这种把布局贴在软键盘上面的做法,其实在运用开发中仍是相对常见的,比方把输入框的Dialog贴在软键盘上面,比方语言查找的布局放在软键盘上面等等。
对这样的计划来说,其实咱们能够尽量的优化一下展现的办法,高版别的手时机更加的丝滑流通,总的来说运用第三种计划仍是不错的,兼容性还能够。
本文用到的一些测验机型为5.0 、6.0、 7.0、 12的这些机型,因为时间精力等原因并没有掩盖全版别和机型,假如咱们有其他的兼容性问题也能谈论区沟通一下。假如有其他或更好的计划也能够谈论区沟通哦。
好了,本文的全部代码与Demo都现已开源。有爱好能够看这儿。项目会继续更新,咱们能够重视一下。
假如感觉本文对你有一点点的启示,还望你能点赞
支持一下,你的支持是我最大的动力。
Ok,这一期就此完结。