关于PopupWindow的简单说明

时间:2022-10-31 12:25:19

最新项目中不仅用到了WindowManager来在机顶盒全屏直播状态下按“菜单”键动态添加一个View,该View包含一个ListView用来显示节目列表;同时也用到了PopupWindow实现了下面的一个t9输入法的页面:

关于PopupWindow的简单说明
点击1到9的某个按钮时候:
关于PopupWindow的简单说明
具体实现方法就不赘述了,就是PopupWindow的简单应用。本篇博客就是简单的说明的是PopupWindow的实现原理。
PopupWindow不是一个Window,只是一个普通的java类(它的直接父类是Object),在分析之前先说一个既定事实:PopupWindow是通过WindowManager对象来添加和删除View的。

  /**
*该方法主要作用就是初始化mContentView
*和WindowManager
*/

public void setContentView(View contentView) {
if (isShowing()) {
return;
}
//初始化PopupWindow的mContentView
mContentView = contentView;

//获取contentView所属的context,并赋值给PopupWindow的mContext变量
if (mContext == null && mContentView != null) {
mContext = mContentView.getContext();
}

//通过context来初始化WindowManager变量
if (mWindowManager == null && mContentView != null) {
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
}
}

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;
//根据token创建一个LayoutParams,在这个方法中token有了用武之地
WindowManager.LayoutParams p = createPopupLayout(token);
p.windowAnimations = computeAnimationResource();
//显示之前的处理操作,主要用来初始化mPopuoView这个View,该View就是最终要
//添加到WindowManager里面的那个View
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;
//添加和显示View
invokePopup(p);
}

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);
//将自定义的view添加到popupViewContainer中去,用来显示出自定义的界面
popupViewContainer.addView(mContentView, listParams);

mPopupView = popupViewContainer;
} else {
//把之前设置好的mContentView重新赋值给mPopupView,这个View就是
//最终要添加到WindowManager里面的那个View
mPopupView = mContentView;
}
.....
}

注意上面初始化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()) {
//关闭popWindow
dismiss();
return true;
}
}
return super.dispatchKeyEvent(event);
} else {
return super.dispatchKeyEvent(event);
}
}

使用PopupViewContainer其实很简单,添加如下一段代码即可:
popupWindow.setBackgroundDrawable(new ColorDrawable(0));
这样按返回键的时候就可以自动关闭PopupWindow.
当然如果在你的应用程序中没有调用setBackgroundDrawable方法的话,那么在处理返回键关闭PopupWindow的时候就需要为你的contentView设置onKeyListener方法或者模仿PopupViewContainer的dispatchKeyEvent来自定义一个View了。

public void dismiss() {
if (isShowing() && mPopupView != null) {
//设置显示标识为false
mIsShowing = false;

unregisterForScrollChanged();

try {
//从WindowManager里面删除mPopupView
mWindowManager.removeView(mPopupView);
} finally {
//如果调用了popupWindow.setBackgroundDrawable(new ColorDrawable(0));
if (mPopupView != mContentView && mPopupView instanceof ViewGroup) {
((ViewGroup) mPopupView).removeView(mContentView);
}
mPopupView = null;
//remove之后要执行的操作,提供了外部接口让用户自己设置关闭窗口后执行那些操作。
if (mOnDismissListener != null) {
mOnDismissListener.onDismiss();
}
}
}
}

同时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;
}
//判断x位置是否更新
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);
}
}

文章最后说一下在弹出PopupWindow的时候按home键的处理问题,如果你按home键的需求是关闭掉对应的Activity的话,你需要监听home键当用户按home键的时候直接调用对应Activity的finish()方法即可,但是如果此时你的页面有PopupWindow存在,如果不做一个处理的话会报如下错误:
关于PopupWindow的简单说明
解决这个问题的方法也很简单,重写Activity的finish方法,在该方法里面调用如下代码即可:

public void dismiss() {
//判断popupWindow是否显示
if(popupWindow.isShowing()) {
popupWindow.dismiss();
}
}

其实我们可以发现不论对View添加、删除还是更新操作最终就是通过WindowManager来完成的,这点需要注意,关于WindowManager的一些简单总结可以看《WindowManager杂谈 》!到此为止本篇博客就结束了,还有一些没有讲到,比如showAtLoaction方法中IBinder参数的作用是用来干什么的?鉴于这个东东本人也暂时还没接触就在这里就不写了,以后再查看大牛的资料研究研究吧。