android PopupWindow点击外部和返回键消失的解决方法
刚接手popupwindow的时候,我们都可能觉得很简单,因为它确实很简单,不过运气不好的可能就会踩到一个坑:
点击popupwindow最外层布局以及点击返回键popupwindow不会消失
新手在遇到这个问题的时候可能会折腾半天,最后通过强大的网络找到一个解决方案,那就是跟popupwindow设置一个背景
popupwindow.setbackgrounddrawable(drawable),这个drawable随便一个什么类型的都可以,只要不为空。
demo地址:smartpopupwindow_jb51.rar
下面从源码(我看的是android-22)上看看到底发生了什么事情导致返回键不能消失弹出框:
先看看弹出框显示的时候代码showasdropdown,里面有个preparepopup方法。
public void showasdropdown(view anchor, int xoff, int yoff, int gravity) { if (isshowing() || mcontentview == null) { return; } registerforscrollchanged(anchor, xoff, yoff, gravity); misshowing = true; misdropdown = true; windowmanager.layoutparams p = createpopuplayout(anchor.getwindowtoken()); preparepopup(p); updateaboveanchor(finddropdownposition(anchor, p, xoff, yoff, gravity)); if (mheightmode < 0) p.height = mlastheight = mheightmode; if (mwidthmode < 0) p.width = mlastwidth = mwidthmode; p.windowanimations = computeanimationresource(); invokepopup(p); }
再看preparepopup方法
/** * <p>prepare the popup by embedding in into a new viewgroup if the * background drawable is not null. if embedding is required, the layout * parameters' height is modified to take into account the background's * padding.</p> * * @param p the layout parameters of the popup's content view */ private void preparepopup(windowmanager.layoutparams p) { if (mcontentview == null || mcontext == null || mwindowmanager == null) { throw new illegalstateexception("you must specify a valid content view by " + "calling setcontentview() before attempting to show the popup."); } if (mbackground != null) { final viewgroup.layoutparams layoutparams = mcontentview.getlayoutparams(); int height = viewgroup.layoutparams.match_parent; if (layoutparams != null && layoutparams.height == viewgroup.layoutparams.wrap_content) { height = viewgroup.layoutparams.wrap_content; } // when a background is available, we embed the content view // within another view that owns the background drawable popupviewcontainer popupviewcontainer = new popupviewcontainer(mcontext); popupviewcontainer.layoutparams listparams = new popupviewcontainer.layoutparams( viewgroup.layoutparams.match_parent, height ); popupviewcontainer.setbackground(mbackground); popupviewcontainer.addview(mcontentview, listparams); mpopupview = popupviewcontainer; } else { mpopupview = mcontentview; } mpopupview.setelevation(melevation); mpopupviewinitiallayoutdirectioninherited = (mpopupview.getrawlayoutdirection() == view.layout_direction_inherit); mpopupwidth = p.width; mpopupheight = p.height; }
上面可以看到mbackground不为空的时候,会popupviewcontainer作为mcontentview的parent,下面看看popupviewcontainer到底干了什么
private class popupviewcontainer extends framelayout { private static final string tag = "popupwindow.popupviewcontainer"; public popupviewcontainer(context context) { super(context); } @override protected int[] oncreatedrawablestate(int extraspace) { if (maboveanchor) { // 1 more needed for the above anchor state final int[] drawablestate = super.oncreatedrawablestate(extraspace + 1); view.mergedrawablestates(drawablestate, above_anchor_state_set); return drawablestate; } else { return super.oncreatedrawablestate(extraspace); } } @override public boolean dispatchkeyevent(keyevent event) { // 这个方法里面实现了返回键处理逻辑,会调用dismiss 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); } } @override public boolean dispatchtouchevent(motionevent ev) { if (mtouchinterceptor != null && mtouchinterceptor.ontouch(this, ev)) { return true; } return super.dispatchtouchevent(ev); } @override public boolean ontouchevent(motionevent event) { // 这个方法里面实现点击消失逻辑 final int x = (int) event.getx(); final int y = (int) event.gety(); if ((event.getaction() == motionevent.action_down) && ((x < 0) || (x >= getwidth()) || (y < 0) || (y >= getheight()))) { dismiss(); return true; } else if (event.getaction() == motionevent.action_outside) { dismiss(); return true; } else { return super.ontouchevent(event); } } @override public void sendaccessibilityevent(int eventtype) { // clinets are interested in the content not the container, make it event source if (mcontentview != null) { mcontentview.sendaccessibilityevent(eventtype); } else { super.sendaccessibilityevent(eventtype); } } }
看到上面红色部分的标注可以看出,这个内部类里面封装了处理返回键退出和点击外部退出的逻辑,但是这个类对象的构造过程中(preparepopup方法中)却有个mbackground != null的条件才会创建
而mbackground对象在setbackgrounddrawable方法中被赋值,看到这里应该就明白一切了。
/** * specifies the background drawable for this popup window. the background * can be set to {@code null}. * * @param background the popup's background * @see #getbackground() * @attr ref android.r.styleable#popupwindow_popupbackground */ public void setbackgrounddrawable(drawable background) { mbackground = background; // 省略其他的 }
setbackgrounddrawable方法除了被外部调用,构造方法中也会调用,默认是从系统资源中取的
/** * <p>create a new, empty, non focusable popup window of dimension (0,0).</p> * * <p>the popup does not provide a background.</p> */ public popupwindow(context context, attributeset attrs, int defstyleattr, int defstyleres) { mcontext = context; mwindowmanager = (windowmanager)context.getsystemservice(context.window_service); final typedarray a = context.obtainstyledattributes( attrs, r.styleable.popupwindow, defstyleattr, defstyleres); final drawable bg = a.getdrawable(r.styleable.popupwindow_popupbackground); melevation = a.getdimension(r.styleable.popupwindow_popupelevation, 0); moverlapanchor = a.getboolean(r.styleable.popupwindow_overlapanchor, false); final int animstyle = a.getresourceid(r.styleable.popupwindow_popupanimationstyle, -1); manimationstyle = animstyle == r.style.animation_popupwindow ? -1 : animstyle; a.recycle(); setbackgrounddrawable(bg); }
有些版本没有,android6.0版本preparepopup如下:
/** * prepare the popup by embedding it into a new viewgroup if the background * drawable is not null. if embedding is required, the layout parameters' * height is modified to take into account the background's padding. * * @param p the layout parameters of the popup's content view */ private void preparepopup(windowmanager.layoutparams p) { if (mcontentview == null || mcontext == null || mwindowmanager == null) { throw new illegalstateexception("you must specify a valid content view by " + "calling setcontentview() before attempting to show the popup."); } // the old decor view may be transitioning out. make sure it finishes // and cleans up before we try to create another one. if (mdecorview != null) { mdecorview.canceltransitions(); } // when a background is available, we embed the content view within // another view that owns the background drawable. if (mbackground != null) { mbackgroundview = createbackgroundview(mcontentview); mbackgroundview.setbackground(mbackground); } else { mbackgroundview = mcontentview; } mdecorview = createdecorview(mbackgroundview); // the background owner should be elevated so that it casts a shadow. mbackgroundview.setelevation(melevation); // we may wrap that in another view, so we'll need to manually specify // the surface insets. final int surfaceinset = (int) math.ceil(mbackgroundview.getz() * 2); p.surfaceinsets.set(surfaceinset, surfaceinset, surfaceinset, surfaceinset); p.hasmanualsurfaceinsets = true; mpopupviewinitiallayoutdirectioninherited = (mcontentview.getrawlayoutdirection() == view.layout_direction_inherit); mpopupwidth = p.width; mpopupheight = p.height; }
这里实现返回键监听的代码是mdecorview = createdecorview(mbackgroundview),这个并没有受到那个mbackground变量的控制,所以这个版本应该没有我们所描述的问题,感兴趣的可以自己去尝试一下
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。