关于PopupWindow的简单说明

来源:互联网 发布:php获取时间轴 编辑:程序博客网 时间:2024/05/17 17:14

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

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存在,如果不做一个处理的话会报如下错误:
这里写图片描述
解决这个问题的方法也很简单,重写Activity的finish方法,在该方法里面调用如下代码即可:

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

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

1 0
原创粉丝点击