博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android开发——如何优雅的将布局置于输入法之上
阅读量:4045 次
发布时间:2019-05-24

本文共 8306 字,大约阅读时间需要 27 分钟。

0. 前言

在Android应用的开发中,有一些产品需求,需要我们获取到输入法的高度。遗憾的是,Android官方并没有提供这样的API。

最近在做的直播项目就有类似的需求,先看一下淘宝的直播页面,当用户点击下方的布局时,弹出输入法的同时,将一个新的EditText置于输入法的正上方,这就需要我们准确的获取到输入法的高度,同时兼顾虚拟按键栏的高度

同时也看到,在输入法出现时,后面的界面的布局没有受到任何的影响,这显然是android:windowSoftInputMode="adjustNothing"的效果。关于windowSoftInputMode的各种属性含义,可以参考。

因此,综上所述,我们需求就是:

  • 在Activity设置为adjustNothing时获取到输入法的准确高度
  • 我们再加一条要求,兼容全面屏手机。不管底部的虚拟操作栏是否存在,都应该准确的将EditText置于输入法的正上方
    在这里插入图片描述

1. 通常的解决方案

由于Android官方并没有提供API让我们准确的获取到输入法的高度,所以我们只能自己想办法实现。

网上的方案一搜一大把,但是效果并不理想,十个里面有九个都是通过监听布局的变化,即监听ViewTreeObserver.OnGlobalLayoutListener这个接口,在弹出输入法后,用屏幕的总高度减去当前页面窗口的显示范围来得到输入法的高度,通常代码如下:

@Overridepublic void onGlobalLayout() {
​ Rect rect = new Rect();​ // 获取当前页面窗口的显示范围​ ((Activity) getContext()).getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);​ int screenHeight = getScreenHeight();​ int keyboardHeight = screenHeight - rect.bottom; // 输入法的高度​ if (Math.abs(keyboardHeight) > screenHeight / 5) {
​ // 超过屏幕五分之一则表示弹出了输入法​ }}

这种方案的劣势很明显:

  • 首先“超过屏幕1/5”这种阈值的设置就略为主观,也有的方案是设置阈值为200个像素,都是同理;
  • 再者,是否只有弹出输入法才会触发onGlobalLayout且窗口显示范围缩小,这里还是得打个问号,不确定是否会有有其他情况导致误判
  • 最重要的是,这种方案需要在Activity的配置为android:windowSoftInputMode="adjustResize"时没有问题,可以正确获取输入法的高度,因为布局在该属性下确实会动态的调整。但是这种调整并不是我们想要的,在我们的直播场景下,不希望输入法影响到布局的任何调整。但是当Activity配置为android:windowSoftInputMode="adjustNothing"时,布局不会在输入法弹出时进行调整,上面的方案自然也会失效

2. 基于“透明Window”的解决方案简介

方案原理简介

这种方案的核心原理为,新建一个宽度为0,高度为MATCH_PARENT且支持adjustResize属性的透明PopupWindow,将其盖在当前Activity的content之上,并通过getViewTreeObserver().addOnGlobalLayoutListener的方式,让PopupWindow感知到布局的变化,这样在输入法弹出之后,我们的PopupWindow就会被resize到一个小于屏幕高度的尺寸,用屏幕高度减去该尺寸,便得到了输入法的高度。该方案因为不涉及我们的Activity容器,因此Activity在配置为android:windowSoftInputMode="adjustNothing"时,该方案同样生效。

3. 基于“透明Window”方案的具体实现

3.1 接口设计

首先定义一个接口,onKeyboardHeightChanged()会在输入法的高度发生变化时调用,参数height<=0时,意味着输入法被收起;height>0意味着输入法被打开,同时height值即为输入法的高度。参数orientation为屏幕方向,备用。

因为在我们的应用案例中,不管底部的虚拟操作栏是否存在,都应该准确的将EditText置于输入法的正上方,所以我们同样得关心虚拟操作栏的高度。因此设计另一个方法onVirtualBottomHeight()。参数height即为虚拟操作栏的高度,为0表示全面屏,即虚拟操作栏不存在的情况。

/** * Created by Calvin on 2020/6/1. */public interface KeyboardHeightObserver {
void onKeyboardHeightChanged(int height, int orientation); void onVirtualBottomHeight(int height);}
3.2 PopupWindow的实现
  • 在KeyboardHeightProvider()构造方法中配置PopupWindow的各种参数,比如adjustResize等重要参数;

  • PopupWindow的showAtLocation()时机实现在start()方法里,这里调用的时机很重要,必须在Activity的onResume之后调用,因此在Activity中需要手动post一下;

  • 监听View树的OnGlobalLayoutListener,将屏幕高度减去PopupWindow的可见高度,得到一个diff的高度diff

    • 1.当diff为负数时,这个数值的相反数刚好等于底部虚拟操作栏的高度,回调给onVirtualBottomHeight()方法使用

    • 2.当diff为0时,表示输入法为收起状态,回调给onKeyboardHeightChanged()方法使用

    • 3.当diff为正数时,表示输入法为打开状态,回调给onKeyboardHeightChanged()方法使用

  • 当计算PopupWindow的可见高度时,用到了getWindowVisibleDisplayFrame()这个方法,这个方法比较复杂,使用时有很多注意点,完全可以另写一篇文章介绍,这里简单介绍四点:

    • 1.它是View类下的一个方法,使用当前窗口中的任意View执行getWindowVisibleDisplayFrame()返回的结果都是一样的,用来获取当前窗口可视区域大小,View是否可见不影响返回结果。
    • 2.只有View对象已经attach到Window上之后,调用此方法才能得到真实的窗口的可视区域大小。值得注意的是,在Activity的onAttachedToWindow()方法和自定义View的onAttachedToWindow()中执行,都不是一个好的时机,前者是因为当前Window被attach到WindowManager中,但是Window中的View仍然没有attach到Window上;后者实际测试结果不稳定。推荐的调用时机可以在onWindowFocusChanged以及onGlobalLayout中
    • 3.如果窗口是全屏的,outRect中的top值始终为0。如果窗口的LayoutParams的height设置为MATCH_PARENT,outRect中的top值等于系统状态栏的高度。如果窗口的LayoutParams的height设置为WRAP_CONTENT或者某个具体的值,且窗口和状态栏存在重叠,outRect中的top值等于重叠区域的高度。同理可应用于输入法/虚拟按键栏与outRect.bottom的关系。比如获取一个高度是MATCH_PARENT的窗口在输入法/虚拟按键栏显示和隐藏两种状态下bottom差值就是输入法/虚拟按键栏的高度
    • 4.getWindowVisibleDisplayFrame()方法是通过IPC方式从WindowManager中获取到这个信息的,相对来说它的开销会比较大,因此不适合放在对性能要求很高的地方调用。
/** * Created by Calvin on 2020/6/1. */public class KeyboardHeightProvider extends PopupWindow {
private KeyboardHeightObserver observer; private int keyboardLandscapeHeight; private int keyboardPortraitHeight; private View popupView; private View parentView; private Activity activity; public KeyboardHeightProvider(Activity activity) {
super(activity); this.activity = activity; LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE); this.popupView = inflater.inflate(R.layout.keyboard_popup_window, null, false); setContentView(popupView); setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_RESIZE | LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); parentView = activity.findViewById(android.R.id.content); setWidth(0); setHeight(LayoutParams.MATCH_PARENT); popupView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
if (popupView != null) {
handleOnGlobalLayout(); } }); } public void start() {
if (!isShowing() && parentView.getWindowToken() != null) {
setBackgroundDrawable(new ColorDrawable(0)); showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0); } } public void close() {
this.observer = null; dismiss(); } public void setKeyboardHeightObserver(KeyboardHeightObserver observer) {
this.observer = observer; } private int getScreenOrientation() {
return activity.getResources().getConfiguration().orientation; } private void handleOnGlobalLayout() {
Point screenSize = new Point(); activity.getWindowManager().getDefaultDisplay().getSize(screenSize); Rect rect = new Rect(); popupView.getWindowVisibleDisplayFrame(rect); int orientation = getScreenOrientation(); int keyboardHeight = screenSize.y - rect.bottom; if (keyboardHeight < 0 && observer != null) {
observer.onVirtualBottomHeight(-keyboardHeight); } if (keyboardHeight == 0) {
notifyKeyboardHeightChanged(0, orientation); } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
this.keyboardPortraitHeight = keyboardHeight; notifyKeyboardHeightChanged(keyboardPortraitHeight, orientation); } else {
this.keyboardLandscapeHeight = keyboardHeight; notifyKeyboardHeightChanged(keyboardLandscapeHeight, orientation); } } private void notifyKeyboardHeightChanged(int height, int orientation) {
if (observer != null) {
observer.onKeyboardHeightChanged(height, orientation); } }}
3.3 Activity中使用

在Activity定义成员变量,并在onCreate()中进行初始化:

private KeyboardHeightProvider mProvider;//虚拟导航栏的高度, 默认为0private int mVirtualBottomHeight;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {
... mProvider = new KeyboardHeightProvider(getActivity()); new Handler().post(() -> mProvider.start());}

为了防止内存泄漏,进行生命周期的相关处理:

@Overrideprotected void onResume() {
super.onResume(); mProvider.setKeyboardHeightObserver(this);}@Overrideprotected void onPause() {
super.onPause(); mProvider.setKeyboardHeightObserver(null);}@Overrideprotected void onDestroy() {
super.onDestroy(); mProvider.close();}

最后是在回调方法中,完成数据的使用:

@Overridepublic void onKeyboardHeightChanged(int height, int orientation) {
if (height > 0) {
//输入法弹出 //输入法之上的布局(包括EditText+发送按钮)在整个屏幕中的位置是沉底的 //这里将布局显示出来, 并设置MarginBottom, 这样就被输入法布局拖起来了 //这里的MarginBottom已经包含了虚拟导航栏的高度mVirtualBottomHeight mInputLayout.setVisibility(View.VISIBLE); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mInputLayout.getLayoutParams(); params.setMargins(0, 0, 0, height + mVirtualBottomHeight); mInputLayout.requestLayout(); } else {
//输入法隐藏, 布局隐藏 mInputLayout.setVisibility(View.GONE); }}@Overridepublic void onVirtualBottomHeight(int height) {
//虚拟导航栏高度赋值 mVirtualBottomHeight = height;}

4. 后续彩蛋

  • 在小米2S机型上出现了一个奇葩的兼容性问题,在PopupWindow中竟然无法触发onGlobalLayout的回调,后确认为系统bug,更新手机系统后解决。

  • 将Activity的adjustNothing属性改成adjustResize后发现功能依然正常,效果同adjustNothing,这就有点奇葩了,我们当初费劲全力使用adjustNothing就是为了不让直播页面Resize,后续经过研究发现:在我们的工程里,为当前直播页面的Activity设置了全屏模式,全屏模式下adjustResize会失效,效果同adjustNothing(但是经过测试弹出输入法后Rect.bottom窗口大小还是变了,adjustNothing模式下是不可能变的)。如果把全面屏代码删掉,adjustResize会让你的界面错乱。这里只能说是歪打正着了,当然全屏模式+adjustNothing肯定也不会有什么问题。

  • 上一条会延伸出一个新的问题:当你在全面屏模式下,需要输入框被顶上来时,即便你设置了adjustResize输入框依然会被输入法遮挡。在本文的业务场景里不需要,如果你有这个需求,可以参考以下三篇参考文章中的解决方案:

转载地址:http://kewci.baihongyu.com/

你可能感兴趣的文章
ubuntu下SVN服务器安装配置
查看>>
MPMoviePlayerViewController和MPMoviePlayerController的使用
查看>>
CocoaPods实践之制作篇
查看>>
[Mac]Mac 操作系统 常见技巧
查看>>
苹果Swift编程语言入门教程【中文版】
查看>>
捕鱼忍者(ninja fishing)之游戏指南+游戏攻略+游戏体验
查看>>
iphone开发基础之objective-c学习
查看>>
iphone开发之SDK研究(待续)
查看>>
计算机网络复习要点
查看>>
Variable property attributes or Modifiers in iOS
查看>>
NSNotificationCenter 用法总结
查看>>
C primer plus 基础总结(一)
查看>>
剑指offer算法题分析与整理(三)
查看>>
Ubuntu 13.10使用fcitx输入法
查看>>
pidgin-lwqq 安装
查看>>
mint/ubuntu安装搜狗输入法
查看>>
C++动态申请数组和参数传递问题
查看>>
opencv学习——在MFC中读取和显示图像
查看>>
Matlab与CUDA C的混合编程配置出现的问题及解决方案
查看>>
如何将PaperDownloader下载的文献存放到任意位置
查看>>