public class SoftKeyBoardListener {
private View rootView;//activity的根视图
int rootViewVisibleHeight;//纪录根视图的显示高度
private OnSoftKeyBoardChangeListener onSoftKeyBoardChangeListener;
public SoftKeyBoardListener(Activity activity) {
//获取activity的根视图
rootView = activity.getWindow().getDecorView();
//监听视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//获取当前根视图在屏幕上显示的大小
Rect r = new Rect();
rootView.getWindowVisibleDisplayFrame(r);
int visibleHeight = r.height();
System.out.println(""+visibleHeight);
if (rootViewVisibleHeight == 0) {
rootViewVisibleHeight = visibleHeight;
return;
}
//根视图显示高度没有变化,可以看作软键盘显示/隐藏状态没有改变
if (rootViewVisibleHeight == visibleHeight) {
return;
}
//根视图显示高度变小超过200,可以看作软键盘显示了
if (rootViewVisibleHeight - visibleHeight > 200) {
if (onSoftKeyBoardChangeListener != null) {
onSoftKeyBoardChangeListener.keyBoardShow(rootViewVisibleHeight - visibleHeight);
}
rootViewVisibleHeight = visibleHeight;
return;
}
//根视图显示高度变大超过200,可以看作软键盘隐藏了
if (visibleHeight - rootViewVisibleHeight > 200) {
if (onSoftKeyBoardChangeListener != null) {
onSoftKeyBoardChangeListener.keyBoardHide(visibleHeight - rootViewVisibleHeight);
}
rootViewVisibleHeight = visibleHeight;
return;
}
}
});
}
private void setOnSoftKeyBoardChangeListener(OnSoftKeyBoardChangeListener onSoftKeyBoardChangeListener) {
this.onSoftKeyBoardChangeListener = onSoftKeyBoardChangeListener;
}
public interface OnSoftKeyBoardChangeListener {
void keyBoardShow(int height);
void keyBoardHide(int height);
}
public static void setListener(Activity activity, OnSoftKeyBoardChangeListener onSoftKeyBoardChangeListener) {
SoftKeyBoardListener softKeyBoardListener = new SoftKeyBoardListener(activity);
softKeyBoardListener.setOnSoftKeyBoardChangeListener(onSoftKeyBoardChangeListener);
}
}
===========================================================================================
public static boolean checkNavigationBarShow(@NonNull Context context, @NonNull Window window) {
boolean show;
Display display = window.getWindowManager().getDefaultDisplay();
Point point = new Point();
display.getRealSize(point);
View decorView = window.getDecorView();
Configuration conf = context.getResources().getConfiguration();
if (Configuration.ORIENTATION_LANDSCAPE == conf.orientation) {
View contentView = decorView.findViewById(android.R.id.content);
show = (point.x != contentView.getWidth());
} else {
Rect rect = new Rect();
decorView.getWindowVisibleDisplayFrame(rect);
show = (rect.bottom != point.y);
}
return show;
}
getWindowVisibleDisplayFrame()的执行结果和View对象选取的关系
由于getWindowVisibleDisplayFrame()方法是View类下的一个方法,所以只能通过View对象来调用。一个窗口中通常都会有多个View,getWindowVisibleDisplayFrame()方法的返回结果和该窗口中选取的View并没有关系。在某个时刻,使用当前窗口中的任意View执行getWindowVisibleDisplayFrame()返回的结果都是一样的。这也很容易理解,getWindowVisibleDisplayFrame()方法返回的是窗口的可视区域大小,并非某个View的可视区域大小,所以用窗口中的任意View来执行都是没有差别的。一般来说可以使用当前窗口的根View来执行这个方法,也就是调用Window对象的getDecorView().getWindowVisibleDisplayFrame()来获取。在Acitivity和Dialog中可以用getWindow()来得到Window对象,合起来就是这样的。
Rect rect = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
这里需要注意的是,由于getWindowVisibleDisplayFrame()方法是用来获取某个窗口的可视区域大小,所以调用getWindowVisibleDisplayFrame()方法的View必须包含在该窗口中,如果是一个孤立的View,或者包含在其他窗口中,是没有意义的。例如
Rect rect = new Rect();
// 这个new出来的View并没有add到任何窗口上,所以调用它的getWindowVisibleDisplayFrame()方法是没有意义的。
new View(this).getWindowVisibleDisplayFrame(rect);
getWindowVisibleDisplayFrame()的执行结果和View对象状态的关系
虽然getWindowVisibleDisplayFrame()的执行结果和窗口中View的选取没有关系,但是却和执行此方法时View的状态有关。由于此方法是用来获取窗口的可视区域大小,所以如果调用此方法时,调用的View对象还没有附着(attach)到任何Window上,那么执行此方法将不会得到实际的某个窗口的可视区域大小,只有View对象已经attach到Window上之后,调用此方法才能得到真实的窗口的可视区域大小。当调用的View对象还没有attach到Window时,getWindowVisibleDisplayFrame()方法会估计出一个可能的可视区域大小,这个大小通常是设备的屏幕尺寸(以像素为单位),由于它并不代表真实的窗口可视区域大小,所以这个数值的意义不大。
由于在Activity/Fragment/Dialog的onCreate()方法中,View对象还没有attach到Window上,所以在onCreate()方法中执行某个View的getWindowVisibleDisplayFrame()方法,是不会得到当前Window实际的可视区域大小的。
在Activity的onAttachedToWindow()方法中执行getWindowVisibleDisplayFrame(),也是得不到当前Window实际的可视区域大小的。因为Activity的onAttachedToWindow()方法执行时,表示当前Window被attach到window manager中,Window中的View仍然没有attach到Window上。
View attach到Window之后,View对象的onAttachedToWindow()方法会被执行,理论上来说,在自定义View的onAttachedToWindow()方法中执行getWindowVisibleDisplayFrame()应该可以得到当前Window实际的可视区域大小,但实际情况却并非如此。在自定义View的onAttachedToWindow()方法中执行getWindowVisibleDisplayFrame()会概率性的出现不同的结果,有时返回的rect对象大小是0,有时则是设备的屏幕尺寸,但都不是当前Window实际的可视区域大小。具体原因未知,没有仔细研究。所以,也不要在View对象的onAttachedToWindow()方法中执行getWindowVisibleDisplayFrame()。
要想得到当前Window实际的可视区域大小,可以在Activity/Fragment/Dialog的onWindowFocusChanged()方法中执行getWindowVisibleDisplayFrame()。这时Window中的View对象都已经attach到Window上。
还有一点需要说明的是,getWindowVisibleDisplayFrame()的执行结果和View是否可见没有关系。View无论是VISIBLE,还是INVISIBLE或者GONE,对getWindowVisibleDisplayFrame()的执行结果没有影响。当View是INVISIBLE的时候,其onDraw()方法不会被调用,当View是GONE的时候,其onDraw()和onLayout()方法不会被调用。但无论是INVISIBLE或者GONE,onAttachedToWindow()都会被调用,也就是说View会被attach到Window上,所以即使View是INVISIBLE或者GONE的,getWindowVisibleDisplayFrame()也能够正确的返回。
getWindowVisibleDisplayFrame()的执行结果分析
这里所说的getWindowVisibleDisplayFrame()执行结果均是指当前Window实际的可视区域大小,对调用的View对象还没有attach到Window时,getWindowVisibleDisplayFrame()方法估计出可视区域大小的情况不做讨论。
getWindowVisibleDisplayFrame()执行结果和以下因素有关
系统状态栏
系统状态栏会影响getWindowVisibleDisplayFrame()执行结果outRect中的top属性的值。
如果窗口是全屏的,也就是设置了flags为WindowManager.LayoutParams.FLAG_FULLSCREEN,或者android:windowFullscreen设置为true,则outRect中的top属性不受状态栏影响,其值始终为0。否则,outRect中的top属性值将会受到系统状态栏的影响。
如果窗口的LayoutParams的height设置为WindowManager.LayoutParams.MATCH_PARENT,则outRect中的top值会等于系统状态栏的高度,如果窗口的LayoutParams的height设置为WindowManager.LayoutParams.WRAP_CONTENT或者某个具体的值,则outRect中的top值会等于系统状态栏和窗口重叠区域的高度,如果没有重叠,则是0。
例如,屏幕高度为1920,窗口高度设置为1900,窗口居中显示。这时窗口上下距离屏幕各有10个像素的距离。假如系统状态栏高度为60,窗口和状态栏的重叠区域的高度就是50。因此,getWindowVisibleDisplayFrame()返回的outRect中的top值为50。
上面这几点可以归结为一点,outRect中的top值等于系统状态栏在理论上会对窗口上方所在位置产生的影响。
如果窗口是全屏的,系统状态栏将无法影响窗口上方位置,因此,outRect中的top值始终为0。如果窗口的LayoutParams的height设置为WindowManager.LayoutParams.MATCH_PARENT,则理论上窗口将到达屏幕最上方的位置,但是由于状态栏的存在,会压迫窗口位置到状态栏下方,因此,outRect中的top值等于系统状态栏的高度。如果窗口的LayoutParams的height设置为WindowManager.LayoutParams.WRAP_CONTENT或者某个具体的值,且窗口和状态栏存在重叠,则这时状态栏同样会试图压迫窗口位置到状态栏下方,其位移就是重叠区域的高度,因此outRect中的top值等于重叠区域的高度。需要注意的是,这里状态栏对窗口位置的影响并不会实际生效,也就是窗口仍然会和状态栏重叠,因此状态栏对窗口位置的影响是一种理论上的,并非一定会生效。
虚拟键盘
虚拟键盘会影响getWindowVisibleDisplayFrame()执行结果outRect中的bottom属性的值。
如果虚拟键盘是隐藏的,则outRect中的bottom属性的值将始终等于屏幕高度(实际上还要减去虚拟按键栏的高度,这里先忽略虚拟按键)。如果虚拟键盘是显示的,outRect中的bottom属性的值将等于屏幕高度减去理论上虚拟键盘会对窗口位置产生的影响。如果窗口高度是MATCH_PARENT的,则outRect中的bottom属性的值将等于屏幕高度减去虚拟键盘的高度。
同样的例子,屏幕高度为1920,窗口高度设置为1900,窗口居中显示。这时窗口上下距离屏幕各有10个像素的距离。假如虚拟键盘高度为600,窗口和虚拟键盘的重叠区域的高度就是590。因此,getWindowVisibleDisplayFrame()返回的outRect中的bottom值为1920 - 590。
虚拟按键栏
虚拟按键栏会影响getWindowVisibleDisplayFrame()执行结果outRect中的bottom属性的值。
这里只考虑软键盘是隐藏的情况,如果软键盘是显示的,则软键盘和虚拟按键栏对outRect中的bottom属性的值的影响将会叠加。
如果虚拟按键栏是隐藏的,则outRect中的bottom属性的值将始终等于屏幕高度。如果虚拟按键是显示的,outRect中的bottom属性的值将等于屏幕高度减去理论上虚拟按键会对窗口位置产生的影响。如果窗口高度是MATCH_PARENT的,则outRect中的bottom属性的值将等于屏幕高度减去虚拟按键的高度。
这里不再举例说明。
综上所述,getWindowVisibleDisplayFrame()执行结果会受到系统状态栏,系统软键盘,系统虚拟按键的影响。
这里需要注意的是,getWindowVisibleDisplayFrame()的结果并不是该窗口实际的大小(虽然它和窗口的大小有一定关系)。例如一个居中显示的对话框,它的实际高度只有500px,它和系统状态栏,系统软键盘,系统虚拟按键栏都没有重叠,那么getWindowVisibleDisplayFrame()的结果就是设备的尺寸大小,而不是该对话框的实际大小。
此外,虽然方法名字中有一个Visible,但是getWindowVisibleDisplayFrame()的结果并不受该窗口是否在被其他窗口遮挡的影响。即使该窗口已经被切换到后台,只要该窗口还没有dettach,getWindowVisibleDisplayFrame()的结果就不会变化。
getWindowVisibleDisplayFrame()的应用
在Android系统中,并没有提供api来获取系统状态栏,系统软键盘和系统虚拟按键栏的高度,但在应用中有时会需要获取这几个数值。由于getWindowVisibleDisplayFrame()返回结果会受到系统状态栏,系统软键盘,系统虚拟按键的影响。因此,这个api常常被用来获取系统状态栏,系统软键盘和系统虚拟按键栏的高度。
对系统状态栏高度,获取一个非全屏,且窗口的LayoutParams的height设置为WindowManager.LayoutParams.MATCH_PARENT的窗口可视区域大小,其top值就是状态栏的高度。
对系统软键盘,获取一个高度是MATCH_PARENT的窗口在软键盘显示和隐藏两种不同状态下的可视区域大小,将bottom值相减就可以得到软键盘的高度。
对系统系统虚拟按键栏,获取一个高度是MATCH_PARENT的窗口在虚拟按键显示和隐藏两种不同状态下的可视区域大小,将bottom值相减就可以得到虚拟按键的高度。
getWindowVisibleDisplayFrame()的性能问题
在getWindowVisibleDisplayFrame()方法的注释中有这样一段
说明getWindowVisibleDisplayFrame()方法是通过IPC方式从window manager中获取到这个信息的,相对来说它的开销会比较大,因此不适合放在对性能要求很高的地方调用,例如View绘制的代码中。