最新项目中不仅用到了WindowManager来在机顶盒全屏直播状态下按“菜单”键动态添加一个View,该View包含一个ListView用来显示节目列表;同时也用到了PopupWindow实现了下面的一个t9输入法的页面:
点击1到9的某个按钮时候:
具体实现方法就不赘述了,就是PopupWindow的简单应用。本篇博客就是简单的说明的是PopupWindow的实现原理。
PopupWindow不是一个Window,只是一个普通的Java类(它的直接父类是Object),在分析之前先说一个既定事实:PopupWindow是通过WindowManager对象来添加和删除View的。
/**
*该方法主要作用就是初始化mContentView
*和WindowManager
*/
public void setContentView(View contentView) {
if (isShowing()) {
return;
}
mContentView = contentView;
if (mContext == null && mContentView != null) {
mContext = mContentView.getContext();
}
if (mWindowManager == null && mContentView != null) {
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
setContentView并不像Activity中一样调用了就可以在页面中把PopupWindow在页面中显示出来。因为上面的代码就没有调用mWindowManager把要显示的View添加到WindowManager中。
其实PopupWindow还真找不到如下的一个方法来快速的显示自定义LayoutParams的View:
public void setContentView(View view,LayoutParams params){
setContentView(view);
mWindowManager.addView(view,params);
}
如果要显示PopupWindow的话还需要调用如下三个方法之一:showAsDropDown(View anchor),showAsDropDown(View anchor, int xoff, int yoff),showAtLocation(View parent, int gravity, int x, int y)。这三个方法最终都会调用invokePopup这个方法来让页面中最终显示PopupWindow,其实这个方法最主要的也就是调用了mWindowManager.addView方法而已,需要注意的是这个方法是private的,外部无法访问:
/***
*本方法的主要是调用mWindowManager.addView方法来让PopupWindow
*在页面上最终展示出来。
*/
private void invokePopup(WindowManager.LayoutParams p) {
if (mContext != null) {
p.packageName = mContext.getPackageName();
}
mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
mWindowManager.addView(mPopupView, p);
}
不过在调用showAtLocation和invokPopup之间又是经过怎么样的处理呢?下面先看看showAtLocation这个方法都做了什么:
/***
*@param parent View 该参数主要用来获取该View对应的WindowToken
*@param x PopupWindow的左上角x坐标
*@param y PopupWindow的左上角y坐标
@param gravity 通常设置为0,类似于Gravity.Top这样的设置,用来控制popWindow的显示位置
*/
public void showAtLocation(View parent, int gravity, int x, int y) {
showAtLocation(parent.getWindowToken(), gravity, x, y);
}
public void showAtLocation(IBinder token, int gravity, int x, int y) {
if (isShowing() || mContentView == null) {
return;
}
unregisterForScrollChanged();
mIsShowing = true;
mIsDropdown = false;
WindowManager.LayoutParams p = createPopupLayout(token);
p.windowAnimations = computeAnimationResource();
preparePopup(p);
if (gravity == Gravity.NO_GRAVITY) {
gravity = Gravity.TOP | Gravity.LEFT;
}
p.gravity = gravity;
p.x = x;
p.y = y;
if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;
invokePopup(p);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
showAtLocation方法中有调用了一个重要的方法preparePopup,该法主要是用来初始化mPopupView,而invokePopup方法中mWindowManager.addView添加的View就是mPopupView这个View:
private void preparePopup(WindowManager.LayoutParams p) {
......
if (mBackground != null) {
....
PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, height
);
popupViewContainer.setBackgroundDrawable(mBackground);
popupViewContainer.addView(mContentView, listParams);
mPopupView = popupViewContainer;
} else {
mPopupView = mContentView;
}
.....
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
注意上面初始化mPopupView的方式有两种,如果mBackgroundd!=null,该方法变量可以通过setBackgroundDrawable来设置,那么mPopupView 就是PopupViewContainer ,PopupViewContainer 是一个FrameLayout的子类,主要是重写了dispatchKeyEvent来处理返回键,当用户按返回键的时候就调用dimiss来关闭PopupWindow:
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (getKeyDispatcherState() == null) {
return super.dispatchKeyEvent(event);
}
if (event.getAction() == KeyEvent.ACTION_DOWN
&& event.getRepeatCount() == 0) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null && state.isTracking(event) && !event.isCanceled()) {
dismiss();
return true;
}
}
return super.dispatchKeyEvent(event);
} else {
return super.dispatchKeyEvent(event);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
使用PopupViewContainer其实很简单,添加如下一段代码即可:
popupWindow.setBackgroundDrawable(new ColorDrawable(0));
这样按返回键的时候就可以自动关闭PopupWindow.
当然如果在你的应用程序中没有调用setBackgroundDrawable方法的话,那么在处理返回键关闭PopupWindow的时候就需要为你的contentView设置onKeyListener方法或者模仿PopupViewContainer的dispatchKeyEvent来自定义一个View了。
public void dismiss() {
if (isShowing() && mPopupView != null) {
mIsShowing = false;
unregisterForScrollChanged();
try {
mWindowManager.removeView(mPopupView);
} finally {
if (mPopupView != mContentView && mPopupView instanceof ViewGroup) {
((ViewGroup) mPopupView).removeView(mContentView);
}
mPopupView = null;
if (mOnDismissListener != null) {
mOnDismissListener.onDismiss();
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
同时PopupWindow也提供了几个update重载方法,除了update()这个无参数方法之外其余的几个update最终都调用了update(int x, int y, int width, int height, boolean force);
不过这些update方法真正在页面上呈现出更新效果的还是因为调用了windowManager的updateViewLayout(View,LayoutParam)方法:
/**
* <p>Updates the position and the dimension of the popup window. Width and
* height can be set to -1 to update location only. Calling this function
* also updates the window with the current popup state as
* described for {@link #update()}.</p>
*更新popupWindow的位置和大小,如果宽度width和高度height传的都是-1,那么只会更新popupWindow的位置
*同理可以说明可以单独的设置width和height来更新popupWindow的宽度“或”高度
* @param x popupWindow 左上角新的x坐标
* @param y popupWindow新的坐标
* @param width popupWindow的新的宽度,如果设置为-1的话就不会更新popupWindow的宽度
* @param height popWindow的新的高度,如果设置为-1的话就不会更新popupWindow的高度
* @param force reposition the window even if the specified position
* already seems to correspond to the LayoutParams 是否强制性更新,如果设置成true的话
*就强制调用windowManager的updateViewLayout方法,设置成false,就会根据其余的四个参数来判断是否进行更新
*
*/
public void update(int x, int y, int width, int height, boolean force) {
if (width != -1) {
mLastWidth = width;
setWidth(width);
}
if (height != -1) {
mLastHeight = height;
setHeight(height);
}
if (!isShowing() || mContentView == null) {
return;
}
WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams();
boolean update = force;
final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth;
if (width != -1 && p.width != finalWidth) {
p.width = mLastWidth = finalWidth;
update = true;
}
final int finalHeight = mHeightMode < 0 ? mHeightMode : mLastHeight;
if (height != -1 && p.height != finalHeight) {
p.height = mLastHeight = finalHeight;
update = true;
}
if (p.x != x) {
p.x = x;
update = true;
}
if (p.y != y) {
p.y = y;
update = true;
}
final int newAnim = computeAnimationResource();
if (newAnim != p.windowAnimations) {
p.windowAnimations = newAnim;
update = true;
}
final int newFlags = computeFlags(p.flags);
if (newFlags != p.flags) {
p.flags = newFlags;
update = true;
}
if (update) {
mWindowManager.updateViewLayout(mPopupView, p);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
文章最后说一下在弹出PopupWindow的时候按home键的处理问题,如果你按home键的需求是关闭掉对应的Activity的话,你需要监听home键当用户按home键的时候直接调用对应Activity的finish()方法即可,但是如果此时你的页面有PopupWindow存在,如果不做一个处理的话会报如下错误:
解决这个问题的方法也很简单,重写Activity的finish方法,在该方法里面调用如下代码即可:
public void dismiss() {
if(popupWindow.isShowing()) {
popupWindow.dismiss();
}
}
其实我们可以发现不论对View添加、删除还是更新操作最终就是通过WindowManager来完成的,这点需要注意,关于WindowManager的一些简单总结可以看《WindowManager杂谈 》!到此为止本篇博客就结束了,还有一些没有讲到,比如showAtLoaction方法中IBinder参数的作用是用来干什么的?