刚接手PopupWindow的时候,我们都可能觉得很简单,因为它确实很简单,不过运气不好的可能就会踩到一个坑:

点击PopupWindow最外层布局以及点击返回键PopupWindow不会消失

新手在遇到这个问题的时候可能会折腾半天,最后通过强大的网络找到一个解决方案,那就是跟PopupWindow设置一个背景

popupWindow.setBackgroundDrawable(drawable),这个drawable随便一个什么类型的都可以,只要不为空。

Demo地址:https://github.com/PopFisher/SmartPopupWindow

下面从源码(我看的是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变量的控制,所以这个版本应该没有我们所描述的问题,感兴趣的可以自己去尝试一下

分析到此为止

PopupWindow 点击外部和返回键无法消失背后的真相(setBackgroundDrawable(Drawable background))的更多相关文章

  1. Unity3D-实现连续点击两次返回键退出游戏(安卓/IOS)

    Unity3D-连续点击两次返回键退出游戏 本文提供全流程,中文翻译.Chinar坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) 1 Count ...

  2. 通过广播关闭应用程序(每个Activity)和连续点击两次返回键关闭应用程序

    对于一个应用程序可能有很多个Activity,可能每个人并不想一个个的去关闭Activity,也有可能忘了,那怎么关闭所有的未关闭的Activity呢,其实有很多方法,但是我最喜欢的一种就是通过广播事 ...

  3. Flutter点击两次返回键退出APP

    在APP中一些页面为了防止用户操作失误点击到返回键导致退出APP,可以设置其一定时间内点击两次返回键才允许退出APP,完成这个功能可以通过WillPopScope和SystemNavigator.po ...

  4. popupWindow设置后完美解决返回键响应无效的方案以及popupWindow背景透明方案

    // 点击其他地方消失 viewPuwAddNew.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouc ...

  5. Android popupwindow和dialog监听返回键

    使用情况: 在activity中,出现了popupwindow和dialog,这个时候,如果点击返回键,它们消失了,但是一些操作还在继续.如:1.进行耗时操作,出现dialog提醒用户等待,这时,按下 ...

  6. 【转】Android实现点击两次返回键退出

    在做安卓应用是我们经常要判断用户对返回键的操作,一般为了防止误操作都是在用户连续按下两次返回键的时候提示用户是否退出应用程序. 第一种实现的基本原理就是,当按下BACK键时,会被onKeyDown捕获 ...

  7. Android Studio 点击两次返回键,退出APP

    该功能的实现没有特别复杂,主要在onKeyDown()事件中实现,直接上代码,如下: //第一次点击事件发生的时间 private long mExitTime; /** * 点击两次返回退出app ...

  8. PopupWindow 点击外部区域无法关闭的问题

    在android4.0/5.0系统上,使用popupWindow时,点击内容外部区域无法关闭,但是在6.0及以上机子上又是正常的. 加下面这句代码: mPopupWindow.setBackgroun ...

  9. 在设置了android:parentActivityName后,点击子Activity返回键,父Activity总会调用OnDestroy()的解决方式

    近期查了非常久这个事情.分享给大家, 原理非常easy,一个Activity在manifet里声明了android:parentActivityName:这时候通过Activity左上角的返回butt ...

随机推荐

  1. 测试EF6.1.3和OrmLite性能

    公司原来用nhibernate,现在考虑要不要换一种ORM,于是找来了EF和ServiceStack.OrmLite.所以就产生了测试这三个性能的要求.对比三个ORM工具的性能,主要是对比ORM配置和 ...

  2. Microservice 微服务的理论模型和现实路径

    两年前接触到了微服务的概念,面对日益膨胀的系统感觉豁然开朗.之后的两年逐步把系统按微服务的架构理念进行了重构,并将业务迁移到了新架构之上.感觉现在差不多是时候写一篇关于微服务的总结文章了. 定义 在 ...

  3. 基于Mono跨平台移动应用开发框架发布Xamarin 3.0

    跨平台移动应用开发框架Xamarin可以让你完全用C#编写你的应用程序,在iOS.Android.Windows Phone 8.Windows8和mac平台上共享相同的代码.你可以重用你最喜欢的.N ...

  4. CSS3 Flexbox不迷路指南

    Flexbox早有耳闻,但是决定切实尝试一番,还是因为看了这条围脖: 我觉得用flexbox可以实现,但是发觉无从下手,属性特性都不了解.趁此机会,学习下. 英文原版参考资料在这里:A Complet ...

  5. SQL Server 数据库备份还原和数据恢复

      认识数据库备份和事务日志备份 数据库备份与日志备份是数据库维护的日常工作,备份的目的是在于当数据库出现故障或者遭到破坏时可以根据备份的数据库及事务日志文件还原到最近的时间点将损失降到最低点. 数据 ...

  6. [转]各种移动GPU压缩纹理的使用方法

    介绍了各种移动设备所使用的GPU,以及各个GPU所支持的压缩纹理的格式和使用方法.1. 移动GPU大全 目前移动市场的GPU主要有四大厂商系列:1)Imagination Technologies的P ...

  7. Objective-C 外观模式--简单介绍和使用

    外观模式(Facade),为子系统中的一组接口提供一个一致的界面,定义一个高层接口,这个接口使得这一子系统更加容易使用. 在以下情况下可以考虑使用外观模式: (1)设计初期阶段,应该有意识的将不同层分 ...

  8. 2013 duilib入门简明教程 -- 完整的自绘标题栏(8)

        看了前面那么多教程,相信对duilib已有基本映像了,我们就快马加鞭,做出一个完整的自绘标题栏吧~     看到下面这个效果图,小伙伴们是不是有点惊呆了呢~O(∩_∩)O~       dui ...

  9. tomcat 的优化配置

    一.关于并发连接量的配置 在tomcat的server.xml配置文件中:将<Connector port="8080" protocol="HTTP/1.1&qu ...

  10. 关闭form上chrome的autofill

    Chrome的autofill会自动找到form中的type=password的元素,然后把这个元素前面的元素当做是用户名,它不在乎这个元素叫什么名字.这样又是注册又是登录,你会发现它自作聪明的aut ...