继续创作,加速生长!这是我参加「日新计划 10 月更文挑战」的第5天,点击查看活动详情

前言

本文咱们会一同复习一下软键盘高度获取的几种办法,布局贴在软键盘上作用的完成与优化

工作是这样的,有一天我逛PDD的时分,发现这样一个作用,

Android软键盘的监听与高度控制的几种方案及常用效果

在查找页面中,假如软件弹起了就会有一个语音查找的布局,当咱们隐藏软键盘之后就隐藏这个布局,

然后我又看了一下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如下:

Android软键盘的监听与高度控制的几种方案及常用效果

需求注意的是办法内部获取导航栏的办法是过时的,部分手时机有问题,可是并没有用它做核算,只是用于一个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如下:

Android软键盘的监听与高度控制的几种方案及常用效果

和第一种计划有异曲同工之妙,都是一个办法,可是思路有所不同,可是这种办法也有一个坑点,便是需求核算状态栏的高度。能够看到第二种计划和第一种计划有一个状态栏高度的偏差,咱们记住处理即可。

三、获取软键盘高度-办法三

之前的文章咱们讲过 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 :不触发

Android软键盘的监听与高度控制的几种方案及常用效果

Huawei SLA-TL10 – Android7.0 : 只触发一次,导致顶部的图片顶上去就下不来了

Android软键盘的监听与高度控制的几种方案及常用效果

所以咱们假如想兼容版别的话,那没办法了,只能出绝招了,咱们就把 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软键盘的监听与高度控制的几种方案及常用效果

通过这样的办法咱们就能完成在 Android R 以上的设备能够有当时的软键盘高度回调,而低版别的会直接回调当时的软键盘需求展现的直接高度。

记住需求判断是否需求处理导航栏的高度哦,就算是R以上的咱们也需求判断是否需求减去导航栏高度的。

四、完成布局悬停在软键盘上面

做好了软键盘的高度核算之后,咱们就能完成对应的布局了,这儿咱们以非翻滚的固定布局为比方。

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

Android软键盘的监听与高度控制的几种方案及常用效果

哎?怎么没作用??别慌,还没开端呢!下面开端上计划。

这儿咱们运用计划一来看看作用:

        Keyboard1Utils.registerKeyboardHeightListener(this) {
            YYLogUtils.w("当时的软键盘高度:$it")
            updateVoiceIcon(it)
        }
    //更新语音图标的位置
    private fun updateVoiceIcon(height: Int) {
        mIvVoice.updateLayoutParams<FrameLayout.LayoutParams> {
            bottomMargin = height
        }
    }

咱们简略的做一个增加距离的特点。作用如下:

Android软键盘的监听与高度控制的几种方案及常用效果

嗯,便是PDD和TB的运用作用了,那之前咱们说的跟着软键盘的动画而动画的那种作用呢?

其实便是运用第三种计划,不过只要在Android11以上才干生效,其实现在Android11的占有率还能够。接下来咱们换一个手机试试。

Android软键盘的监听与高度控制的几种方案及常用效果

没什么作用?是的,我还没换呢,闹个眼子。先发一个作用一的图来做一下比照嘛。

接下来咱们运用计划三来试试:

      Keyboard3Utils.registerKeyboardHeightListener(this) {
            YYLogUtils.w("第三种办法:当时的软键盘高度:$it")
            updateVoiceIcon(it)
        }
    //更新语音图标的位置
    private fun updateVoiceIcon(height: Int) {
        mIvVoice.updateLayoutParams<FrameLayout.LayoutParams> {
            bottomMargin = height
        }
    }

作用三的运转作用如下:

Android软键盘的监听与高度控制的几种方案及常用效果

这么看能看出作用一和作用三之间的差异吗,沿着软键盘做的位移,因为我是手机录屏MP4转码GIF,所以是渣渣画质,实际作用比GIF要流通。defu纵享丝滑!

总结

本文的示例都是根据固定布局下的一些软键盘的操作,而假如是ScrollView相似的一些翻滚布局下,那么又是另外一种做法,这儿没有做比照。因为篇幅原因,后期可能会单独出各种布局下软键盘的与EidtText的位置相关设置。(文章已出,有爱好能够看看这篇文章【传送门】)

话说回来,其实这种把布局贴在软键盘上面的做法,其实在运用开发中仍是相对常见的,比方把输入框的Dialog贴在软键盘上面,比方语言查找的布局放在软键盘上面等等。

对这样的计划来说,其实咱们能够尽量的优化一下展现的办法,高版别的手时机更加的丝滑流通,总的来说运用第三种计划仍是不错的,兼容性还能够。

本文用到的一些测验机型为5.0 、6.0、 7.0、 12的这些机型,因为时间精力等原因并没有掩盖全版别和机型,假如咱们有其他的兼容性问题也能谈论区沟通一下。假如有其他或更好的计划也能够谈论区沟通哦。

好了,本文的全部代码与Demo都现已开源。有爱好能够看这儿。项目会继续更新,咱们能够重视一下。

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

Ok,这一期就此完结。

Android软键盘的监听与高度控制的几种方案及常用效果