关于PopupWindow的简单说明

本文详细解析了PopupWindow的实现机制,包括如何初始化内容视图、通过WindowManager添加视图、处理显示与关闭逻辑等,并介绍了如何响应按键事件及更新PopupWindow。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最新项目中不仅用到了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;
        }
        //初始化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);
        }
    }

 
  • 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);
}
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

如果要显示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);
    }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

不过在调用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);
    }

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

            mPopupView = popupViewContainer;
        } else {
            //把之前设置好的mContentView重新赋值给mPopupView,这个View就是
            //最终要添加到WindowManager里面的那个View
            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()) {
                        //关闭popWindow
                        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) {
            //设置显示标识为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();
                }
            }
        }
    }
 
  • 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;
        }
        //判断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);
        }
    }

 
  • 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() {
                //判断popupWindow是否显示
        if(popupWindow.isShowing()) {
            popupWindow.dismiss();
        }
    }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

其实我们可以发现不论对View添加、删除还是更新操作最终就是通过WindowManager来完成的,这点需要注意,关于WindowManager的一些简单总结可以看《WindowManager杂谈 》!到此为止本篇博客就结束了,还有一些没有讲到,比如showAtLoaction方法中IBinder参数的作用是用来干什么的?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值