1. 程式人生 > >安卓獲取輸入法高度與ViewTreeObserver講解

安卓獲取輸入法高度與ViewTreeObserver講解

ans pixel rect pri 定義變量 edr 有關 runnable 寬高

目錄

  • 安卓獲取輸入法高度
    • 前言
    • 清單
    • 開始
  • ViewTreeObserver講解
    • 獲取輸入法高度原理
      • 思路
      • 實現
    • 關於ViewTreeObserver
      • 定義
      • 繼承
      • 摘要
      • 獲取View高度的三種方法
  • 源碼
    • interface KeyboardHeightObserver
    • class KeyboardHeightProvider

為了方便部分精力少的朋友, 本文開始就直接介紹安卓獲取輸入法高度的方法,然後再逐步講解。

安卓獲取輸入法高度

前言

在某些場景下, 比如寫一個聊天界面,包括輸入框和發送以及上面的消息列表,簡單的使用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.

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

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

安卓獲取輸入法高度與ViewTreeObserver講解