安卓获取输入法高度与ViewTreeObserver讲解

本文详细介绍了一种在安卓系统中获取输入法高度的方法,利用PopupWindow和ViewTreeObserver监听全局布局变化,间接计算输入法高度,适用于聊天界面等场景。

为了方便部分精力少的朋友, 本文开始就直接介绍安卓获取输入法高度的方法,然后再逐步讲解。

安卓获取输入法高度

前言

在某些场景下, 比如写一个聊天界面,包括输入框和发送以及上面的消息列表,简单的使用LinearLayout或者RelativeLayout布局,当点击输入框,键盘弹起后,通常是不会遮挡输入框和发送的(有时就比较蛋疼了,不知为啥,它就是遮挡),因为它们也随键盘弹了起来。但布局再复杂点,比如说再加个表情栏或者更多栏,这样你肯定要手动控制输入框的高度了。因此,你就必须手动控制输入框的升降,但问题是升多高呢???这时,就要想办法获取输入法高度了(~ ̄▽ ̄)~

由于目前安卓上还没有提供直接获取输入法高度的api,因此只好我们自己想办法获取它的高度了。

注: 此思路由国外一大神提出,附上他的 Github ;

清单

这里有两个文件:

  • interface KeyboardHeightObserver
  • class KeyboardHeightProvider

前一个用在待观测页面的作为回调函数, 后面是主要的方法所在的类了。

开始

文章后面会附上源码,引入这两个文件后,在要获取输入法高度的页面,首先实现接口KeyboardHeightObserver,即第一个文件,并重写里面的方法;

然后再定义变量 KeyboardHeightProvider keyboardHeightProvider;
实例化

     /**
     * Construct a new KeyboardHeightProvider
     *
     * @param activity The parent activity
     * @param layoutId   R.layout.*
     */
       // 以上为构造函数的相关注释,当然这里是我修改的,这样可以同时支持观测多个页面
       keyboardHeightProvider = new KeyboardHeightProvider(this, R.layout.activity_chat);

       new Handler().post(new Runnable() {
            @Override
            public void run() {
                keyboardHeightProvider.start();
            }
        });

这时还要在onStart()函数里面加上 keyboardHeightProvider.setKeyboardHeightObserver(this); 即:

    @Override
    public void onStart() {
        super.onStart();
        // 这里使用了刚才实现的接口
        keyboardHeightProvider.setKeyboardHeightObserver(this);
    }

考虑更全的话, 还可以加上以下语句:

    @Override
    public void onPause() {
        super.onPause();
        keyboardHeightProvider.setKeyboardHeightObserver(null);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        keyboardHeightProvider.close();
    }
      

这样一来,在回调函数 onKeyboardHeightChanged里面就回收到回调结果了,大功告成!

ViewTreeObserver讲解

这里就结合上面输入法的例子,讲讲ViewTreeObserver。

获取输入法高度原理

思路

在要获取输入法高度的页面,创建一个看不见的弹窗,即宽为0,高为全屏,并为弹窗设置全局布局监听器。当布局有变化,比如有输入法弹窗出现或消失时, 监听器回调函数就会被调用。而其中的关键就是当输入法弹出时, 它会把之前我们创建的那个看不见的弹窗往上挤, 这样我们创建的那个弹窗的位置就变化了,只要获取它底部高度的变化值就可以间接的获取输入法的高度了。

实现

首先创建类KeyboardHeightProvider, 继承自PopupWindow;

然后构造器内完成相关初始化:


        super(activity);
        this.activity = activity;

       LayoutInflater inflator = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        this.popupView = inflator.inflate(layoutId, 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的观测者(感觉用 ViewTreeObserver还是更合适)设置全局布局监听器

        popupView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (popupView != null) {
                    handleOnGlobalLayout();
                }
            }
        });

其中handleOnGlobalLayout函数功能则是:获取弹窗高度,并作差得出输入法高度,以及通知回调。

    /**
     * Popup window itself is as big as the window of the Activity.
     * The keyboard can then be calculated by extracting the popup view bottom
     * from the activity window height.
     */
    private void handleOnGlobalLayout() {

        Point screenSize = new Point();
        activity.getWindowManager().getDefaultDisplay().getSize(screenSize);

        Rect rect = new Rect();
        popupView.getWindowVisibleDisplayFrame(rect);

        // REMIND, you may like to change this using the fullscreen size of the phone
        // and also using the status bar and navigation bar heights of the phone to calculate
        // the keyboard height. But this worked fine on a Nexus.
        int orientation = getScreenOrientation();
        int keyboardHeight = screenSize.y - rect.bottom;

        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);
        }
    }

嗯,大概就是这样(* ̄3 ̄)╭

关于ViewTreeObserver

定义

首先自然要给出官方的定义:

/**
 * A view tree observer is used to register listeners that can be notified of global
 * changes in the view tree. Such global events include, but are not limited to,
 * layout of the whole tree, beginning of the drawing pass, touch mode change....
 *
 * A ViewTreeObserver should never be instantiated by applications as it is provided
 * by the views hierarchy. Refer to {@link android.view.View#getViewTreeObserver()}
 * for more information.
 */

翻译过来大概是

// 原谅我英语不好(╯︿╰), 不过我发现谷歌翻译的效果还是不错的

/**
* 视图树观察器用于注册可以在视图树中通知全局
* 更改的侦听器。此类全局事件包括但不限于
* 整个树的布局,绘图过程的开始,触摸模式更改.... 
* 
* ViewTreeObserver永远不应由应用程序实例化,因为它由视图层次结构提供
* 。有关更多信息,请参阅{@link android.view.View#getViewTreeObserver()}
* 。 
*/

继承

java.lang.Object
   ↳    android.view.ViewTreeObserver

直接继承自Object,没有另外的继承关系

摘要

Nested Classes
interfaceViewTreeObserver.OnDrawListenerInterface definition for a callback to be invoked when the view tree is about to be drawn.
interfaceViewTreeObserver.OnGlobalFocusChangeListenerInterface definition for a callback to be invoked when the focus state within the view tree changes.
interfaceViewTreeObserver.OnGlobalLayoutListenerInterface definition for a callback to be invoked when the global layout state or the visibility of views within the view tree changes.
interfaceViewTreeObserver.OnPreDrawListenerInterface definition for a callback to be invoked when the view tree is about to be drawn.
interfaceViewTreeObserver.OnScrollChangedListenerInterface definition for a callback to be invoked when something in the view tree has been scrolled.
interfaceViewTreeObserver.OnTouchModeChangeListenerInterface definition for a callback to be invoked when the touch mode changes.

另外方法挺多的, 我就不列举了。

获取View高度的三种方法

注: 此处参考了小马快跑 的博客

在某些时候,我们要获取view的高度,但获取到的为0,为什么呢?这样通常时由于页面还未测量导致的,比如在onCreate中调用的话就会直接返回0。这是就需要我们手动获取了。

View的MeasureSpec.UNSPECIFIED

通过设置View的MeasureSpec.UNSPECIFIED来测量:

int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
view.measure(w, h);
//获得宽高
int viewWidth=view.getMeasuredWidth();
int viewHeight=view.getMeasuredHeight();

设置我们的SpecMode为UNSPECIFIED,然后去调用onMeasure测量宽高,就可以得到宽高。

ViewTreeObserver .addOnGlobalLayoutListener

通过ViewTreeObserver .addOnGlobalLayoutListener来获得宽高,当获得正确的宽高后,请移除这个观察者,否则回调会多次执行:

//获得ViewTreeObserver 
ViewTreeObserver observer=view.getViewTreeObserver();
//注册观察者,监听变化
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
     @Override
     public void onGlobalLayout() {
            //判断ViewTreeObserver 是否alive,如果存活的话移除这个观察者
           if(observer.isAlive()){
             observer.removeGlobalOnLayoutListener(this);
             //获得宽高
             int viewWidth=view.getMeasuredWidth();
             int viewHeight=view.getMeasuredHeight();
           }
        }
   });
ViewTreeObserver .addOnPreDrawListener

通过ViewTreeObserver .addOnPreDrawListener来获得宽高,在执行onDraw之前已经执行了onLayout()和onMeasure(),可以得到宽高了,当获得正确的宽高后,请移除这个观察者,否则回调会多次执行

//获得ViewTreeObserver 
ViewTreeObserver observer=view.getViewTreeObserver();
//注册观察者,监听变化
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
       @Override
       public boolean onPreDraw() {
          if(observer.isAlive()){
            observer.removeOnDrawListener(this);
             }
          //获得宽高
           int viewWidth=view.getMeasuredWidth();
           int viewHeight=view.getMeasuredHeight();
           return true;
     }
   });

源码

interface KeyboardHeightObserver


public interface KeyboardHeightObserver {
    /**
     * Called when the keyboard height has changed, 0 means keyboard is closed,
     * >= 1 means keyboard is opened.
     *
     * @param height        The height of the keyboard in pixels
     * @param orientation   The orientation either: Configuration.ORIENTATION_PORTRAIT or
     *                      Configuration.ORIENTATION_LANDSCAPE
     */
    void onKeyboardHeightChanged(int height, int orientation);
}

class KeyboardHeightProvider


import android.app.Activity;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.PopupWindow;

/**
 * The keyboard height provider, this class uses a PopupWindow
 * to calculate the window height when the floating keyboard is opened and closed.
 */
public class KeyboardHeightProvider extends PopupWindow {

    /** The tag for logging purposes */
    private final static String TAG = "sample_KeyboardHeightProvider";

    /** The keyboard height observer */
    private KeyboardHeightObserver observer;

    /** The cached landscape height of the keyboard */
    private int keyboardLandscapeHeight;

    /** The cached portrait height of the keyboard */
    private int keyboardPortraitHeight;

    /** The view that is used to calculate the keyboard height */
    private View popupView;

    /** The parent view */
    private View parentView;

    /** The root activity that uses this KeyboardHeightProvider */
    private Activity activity;

    /**
     * Construct a new KeyboardHeightProvider
     *
     * @param activity The parent activity
     * @param layoutId   R.layout.*
     */
    public KeyboardHeightProvider(Activity activity, int layoutId) {
        super(activity);
        this.activity = activity;

        LayoutInflater inflator = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        this.popupView = inflator.inflate(layoutId, 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();
                }
            }
        });
    }

    /**
     * Start the KeyboardHeightProvider, this must be called after the onResume of the Activity.
     * PopupWindows are not allowed to be registered before the onResume has finished
     * of the Activity.
     */
    public void start() {

        if (!isShowing() && parentView.getWindowToken() != null) {
            setBackgroundDrawable(new ColorDrawable(0));
            showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0);
        }
    }

    /**
     * Close the keyboard height provider,
     * this provider will not be used anymore.
     */
    public void close() {
        this.observer = null;
        dismiss();
    }

    /**
     * Set the keyboard height observer to this provider. The
     * observer will be notified when the keyboard height has changed.
     * For example when the keyboard is opened or closed.
     *
     * @param observer The observer to be added to this provider.
     */
    public void setKeyboardHeightObserver(KeyboardHeightObserver observer) {
        this.observer = observer;
    }

    /**
     * Get the screen orientation
     *
     * @return the screen orientation
     */
    private int getScreenOrientation() {
        return activity.getResources().getConfiguration().orientation;
    }

    /**
     * Popup window itself is as big as the window of the Activity.
     * The keyboard can then be calculated by extracting the popup view bottom
     * from the activity window height.
     */
    private void handleOnGlobalLayout() {

        Point screenSize = new Point();
        activity.getWindowManager().getDefaultDisplay().getSize(screenSize);

        Rect rect = new Rect();
        popupView.getWindowVisibleDisplayFrame(rect);

        // REMIND, you may like to change this using the fullscreen size of the phone
        // and also using the status bar and navigation bar heights of the phone to calculate
        // the keyboard height. But this worked fine on a Nexus.
        int orientation = getScreenOrientation();
        int keyboardHeight = screenSize.y - rect.bottom;

        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);
        }
    }
}

转载于:https://www.cnblogs.com/asche/p/10104459.html

<think>我们参考了用户提供的两个引用,它们描述了在Android中获取输入法高度的传统方法。然而,用户特别询问了在Android15(即最新版本)中的方法。我们知道,随着Android版本的更新,有些API可能会发生变化,但通常新版本会兼容旧的方法。引用[1]中提到了一个在即使设置了adjustNothing时也能获取输入法高度的方法。引用[2]则介绍了使用WindowInsets来获取系统窗口嵌入物(包括输入法)遮挡的距离,这是一种更现代、更推荐的方式。在Android中,从API20(Android4.4W)开始,引入了WindowInsetsCompat,并且在后续版本中不断改进。特别是从Android11(API30)开始,WindowInsetsAPI有了很大的改进,提供了更简单的方式来监听输入法高度的变化。在Android15(如果按照命名惯例,可能是API35或更高,但具体API级别需要查阅官方文档)中,获取输入法高度的推荐方法应该延续了Android11以来的现代方式,即使用WindowInsets。具体步骤:1.在Activity的onCreate中,或者在你需要监听的View上设置一个OnApplyWindowInsetsListener。2.当窗口的insets发生变化(比如输入法弹出或收起)时,回调会被触发。3.在回调中,我们可以通过WindowInsets对象的getInsets(int)方法(或者使用WindowInsetsCompat来兼容旧版本)来获取不同类型的insets。对于输入法,我们使用WindowInsetsCompat.Type.ime()。注意:由于用户提到Android15,我们可以假设使用最新的API,而无需太多考虑旧版本兼容(除非用户有特殊要求)。下面提供一个使用ViewCompat的示例,它使用了WindowInsetsCompat,因此可以向后兼容。代码示例:```kotlin//假设我们有一个根布局,比如ConstraintLayout,id为root_layoutvalrootLayout=findViewById<View>(R.id.root_layout)ViewCompat.setOnApplyWindowInsetsListener(rootLayout){view,windowInsets->valimeInsets=windowInsets.getInsets(WindowInsetsCompat.Type.ime())valimeHeight=imeInsets.bottom//如果输入法没有显示,imeHeight将是0//否则,imeHeight就是输入法高度(单位是像素)//你可以在这里使用imeHeight,例如调整布局//记得返回处理后的WindowInsetswindowInsets}```或者,如果你在Activity中,并且想要监听整个窗口的insets变化,你可以这样:```kotlinViewCompat.setOnApplyWindowInsetsListener(window.decorView){view,windowInsets->valimeInsets=windowInsets.getInsets(WindowInsetsCompat.Type.ime())valimeHeight=imeInsets.bottom//...同上windowInsets}```注意:从Android11(API级别30)开始,WindowInsets提供了更稳定的方法来获取输入法高度。但上述代码在Android15上同样适用,因为WindowInsetsCompat会适配最新的系统行为。另外,如果你想要实时监听输入法高度的变化(例如在输入法动态调整大小时),这种方法也会被多次调用。所以,在Android15中,使用WindowInsetsCompat来获取输入法高度是推荐的方法。它能够适应不同的系统版本,并且可以其他系统栏(如导航栏)的变化统一处理。如果用户只针对Android15(即最新的API)并且不需要考虑旧版本,也可以直接使用WindowInsets,而不使用WindowInsetsCompat。但是,为了代码的健壮性,通常建议使用兼容库。总结:获取输入法高度的关键API是WindowInsets,通过监听WindowInsets的变化,并从中提取ime类型的insets的bottom值。请注意,用户还需要在manifest文件中为Activity配置合适的windowSoftInputMode,以确保输入法能够正确调整布局,从而使得WindowInsets能够被正确计算。参考官方文档:[WindowInsetsCompat](https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat)[WindowInsets](https://developer.android.com/reference/android/view/WindowInsets)但是,由于用户提到Android15,我们假设用户可能使用的是即将发布的新系统,因此我们也可以直接使用Android框架中的WindowInsets,而无需依赖兼容库。不过,目前Android15的API尚未完全确定,但是按照惯例,WindowInsets的基本使用方式应该是一致的。在最新版本的Android中(包括Android15预览版),以下是非兼容库的写法(仅适用于API级别>=30):```kotlinvalview=window.decorViewview.setOnApplyWindowInsetsListener{v,insets->valimeHeight=insets.getInsetsIgnoringVisibility(WindowInsets.Type.ime()).bottom//或者使用insets.getInsets(WindowInsets.Type.ime()),但是getInsets会考虑可见性,也就是说如果输入法当前没有显示,那么bottom可能是0。//注意:通常我们关心的是输入法弹出时的高度,所以当输入法弹出时,这个值就是高度,收起来时就是0。//但是,如果你在输入法显示时想要获取高度,那么应该使用getInsets,因为它包含了当前可见的insets。//而getInsetsIgnoringVisibility则不考虑可见性,会返回系统默认的输入法高度(可能不管输入法当前是否可见)。所以根据需求选择。//处理高度...insets}```但是,请注意,如果用户需要兼容较旧版本的Android(比如低于API30),那么应该使用WindowInsetsCompat。因此,我们建议用户根据自己应用的最低API级别来选择。如果最低API级别是30或以上,可以直接使用上述非兼容库的写法。否则,使用WindowInsetsCompat。由于问题针对Android15,那么最低API级别肯定是30以上(因为Android15的API级别至少是35),所以直接使用非兼容库的方法也是可以的。但是,为了代码的健壮性和未来可能的变化,推荐使用JetpackCore库中的WindowInsetsCompat,因为它会随着新API的发布而更新,并且可以在新特性出现时提供一致的行为。所以,最终推荐的方法还是使用WindowInsetsCompat。在build.gradle文件中添加依赖(如果尚未添加):```dependencies{implementation"androidx.core:core-ktx:1.9.0"//或更高版本}```然后使用WindowInsetsCompat。这样,无论Android15将来如何变化,只要WindowInsetsCompat更新了适配,用户代码就不用改变。另外,用户可能还需要注意,在Android11及以上版本中,系统支持输入法改变高度(例如从全屏模式切换到非全屏模式),所以实时监听变化是很重要的。因此,我们给出在Android15(以及Android11及更高版本)中获取输入法高度的推荐方法是使用WindowInsetsCompat。如果用户需要,还可以将输入法高度存储到ViewModel或SharedPreferences中,以便在应用的其他部分使用。如果用户需要精确的输入法高度,这个方法是非常可靠的。最后,用户还需要注意,在Activity创建后,初始状态下可能没有insets信息(输入法没有弹出时bottom为0)。当输入法弹出时,监听器会被触发,从而得到非0的高度值。综上所述,在Android15中,获取输入法高度的最佳方式是使用WindowInsetsCompat监听ime类型的insets,并取其bottom值。</think> 在 Android 15 中获取输入法高度的推荐方式是使用 **WindowInsets API**(特别是 `WindowInsetsCompat`),这种方法可以可靠地检测输入法高度变化,兼容不同系统版本和设备类型: ### ✅ 推荐解决方案 (API 34+) ```kotlin // 在 Activity 的 onCreate() 中: ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, insets -> val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom if (imeBottom > 0) { Log.d("Keyboard", "输入法高度: $imeBottom 像素") } insets } // 或通过视图监听(推荐在主布局中使用) ViewCompat.setOnApplyWindowInsetsListener(binding.rootLayout) { view, insets -> val keyboardHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom // 更新布局或存储高度值 insets } ``` ### 关键特性: 1. **实时监听** - 自动响应输入法弹出/收起事件 2. **像素级精度** - 直接获取系统计算的物理像素高度 3. **设备兼容** - 自动处理刘海屏/折叠屏/手势导航区 4. **无需权限** - 系统级 API 直接调用 ### 原理说明: `WindowInsets.Type.ime()` 标记系统输入法区域,通过监听 `systemWindowInsetBottom` 的变化,可准确获得以下数据: ```math 键盘高度 = 屏幕高度 - 可用区域高度 + 系统栏高度 ``` ### 注意事项: 1. 确保布局设置:`android:fitsSystemWindows="true"` 2. Activity 主题建议使用 `@style/Theme.Material3.DayNight.NoActionBar` 3. 获取值需在视图树布局完成后(在 `onResume()` 或使用 `ViewTreeObserver`) --- ### 备选方案(旧设备兼容) 若需兼容 Android 4.4+(API 19+): ```java final View contentView = findViewById(android.R.id.content); contentView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { Rect r = new Rect(); contentView.getWindowVisibleDisplayFrame(r); int screenHeight = contentView.getRootView().getHeight(); int keyboardHeight = screenHeight - (r.bottom - r.top); if (keyboardHeight > screenHeight * 0.15) { // 有效输入法高度 } }); ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值