安卓獲取輸入法高度與ViewTreeObserver講解
目錄
為了方便部分精力少的朋友, 本文開始就直接介紹安卓獲取輸入法高度的方法,然後再逐步講解。
安卓獲取輸入法高度
前言
在某些場景下, 比如寫一個聊天介面,包括輸入框和傳送以及上面的訊息列表,簡單的使用LinearLayout或者RelativeLayout佈局,當點選輸入框,鍵盤彈起後,通常是不會遮擋輸入框和傳送的(有時就比較蛋疼了,不知為啥,它就是遮擋),因為它們也隨鍵盤彈了起來。但佈局再複雜點,比如說再加個表情欄或者更多欄,這樣你肯定要手動控制輸入框的高度了。因此,你就必須手動控制輸入框的升降,但問題是升多高呢???這時,就要想辦法獲取輸入法高度了(~ ̄▽ ̄)~
由於目前安卓上還沒有提供直接獲取輸入法高度的api,因此只好我們自己想辦法獲取它的高度了。
注: 此思路由國外一大神提出,附上他的ofollow,noindex" target="_blank">Github ;
清單
這裡有兩個檔案:
- interface KeyboardHeightObserver
- class KeyboardHeightProvider
前一個用在待觀測頁面的作為回撥函式, 後面是主要的方法所在的類了。
開始
文章後面會附上原始碼,引入這兩個檔案後,在要獲取輸入法高度的頁面,首先實現介面KeyboardHeightObserver,即第一個檔案,並重寫裡面的方法;
然後再定義變數 KeyboardHeightProvider keyboardHeightProvider;
例項化
/** * Construct a new KeyboardHeightProvider * * @param activity The parent activity * @param layoutIdR.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 heightThe height of the keyboard in pixels * @param orientationThe 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 layoutIdR.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); } } }