V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
asche910
V2EX  ›  Android

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

  •  
  •   asche910 · 2018-12-11 21:46:09 +08:00 · 7706 次点击
    这是一个创建于 2181 天前的主题,其中的信息可能已经有所发展或是发生改变。

    [TOC]

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

    安卓获取输入法高度

    前言

    在某些场景下, 比如写一个聊天界面,包括输入框和发送以及上面的消息列表,简单的使用 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 | | --- | ---- | ---- | | interface | ViewTreeObserver.OnDrawListener | Interface definition for a callback to be invoked when the view tree is about to be drawn. | | interface | ViewTreeObserver.OnGlobalFocusChangeListener | Interface definition for a callback to be invoked when the focus state within the view tree changes. | | interface | ViewTreeObserver.OnGlobalLayoutListener | Interface definition for a callback to be invoked when the global layout state or the visibility of views within the view tree changes. | | interface | ViewTreeObserver.OnPreDrawListener | Interface definition for a callback to be invoked when the view tree is about to be drawn. | | interface | ViewTreeObserver.OnScrollChangedListener | Interface definition for a callback to be invoked when something in the view tree has been scrolled. | | interface | ViewTreeObserver.OnTouchModeChangeListener | Interface definition for a callback to be invoked when the touch mode changes. |

    这里的 markdown 语法不知为啥没用。。。排版完善的可以参考这里

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

    获取 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);
            }
        }
    }
    
    
    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1051 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 19:26 · PVG 03:26 · LAX 11:26 · JFK 14:26
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.