刚接手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. PHP的日期和时间处理函数

    1. 将日期和时间转变为时间戳 1.1 time() 原型:time(void) 作用:返回当前时间的 UNIX时间戳. 参数:void,可选(即无参数) 1.2 mktime() 原型:int mk ...

  2. ios使用CocoaHTTPServer实现文件共享

    CocoaHTTPServer下载地址:https://github.com/robbiehanson/CocoaHTTPServer 实现效果:在电脑端输入192.168.0.100:8080,出现 ...

  3. .NET不可变集合已经正式发布

    微软基础类库(Base Class Library)团队已经完成了.NET不可变集合的正式版本,但不包括ImmutableArray.与其一起发布的还包括针对其它不可变对象类型的设计指南. 如果你需要 ...

  4. 免费图片存储和图话【提供demo下载】

    我们不管是做博客系统还是其他网站,图片是免不了要使用到的.但是,我们都知道图片的访问是很耗资源的,同时也是很占磁盘空间的,且还特别占带宽. 所以,我们一般都会用到特定的图片服务器.不过,像我等屌丝平时 ...

  5. Expert 诊断优化系列------------------语句调优三板斧

    前面三篇通过CPU.内存.磁盘三巨头,讲述了如何透过现在看本质,怎样定位服务器三巨头反映出的问题.为了方便阅读给出链接: SQL SERVER全面优化-------Expert for SQL Ser ...

  6. 【优雅代码】深入浅出 妙用Javascript中apply、call、bind

    这篇文章实在是很难下笔,因为网上相关文章不胜枚举. 巧合的是前些天看到阮老师的一篇文章的一句话: “对我来说,博客首先是一种知识管理工具,其次才是传播工具.我的技术文章,主要用来整理我还不懂的知识.我 ...

  7. 剑指Offer面试题:4.从尾到头打印链表

    一.题目:从尾到头打印链表 题目:输入一个链表的头结点,从尾到头反过来打印出每个结点的值. 到解决这个问题肯定要遍历链表.遍历的顺序是从头到尾的顺序,可输出的顺序却是从尾到头.也就是说第一个遍历到的结 ...

  8. Some practices to write better C#/.NET code(译)

    C#(.NET)中有关编码的一些建议,原文地址:http://www.codeproject.com/Articles/539179/Some-practices-to-write-better-Cs ...

  9. AngularJS 中的Promise --- $q服务详解

    先说说什么是Promise,什么是$q吧.Promise是一种异步处理模式,有很多的实现方式,比如著名的Kris Kwal's Q还有JQuery的Deffered. 什么是Promise 以前了解过 ...

  10. 一个Java程序员的实习总结(2)

    在今天的总结里,主要讲述第二.三周这半个月的培训情况,并且穿插讲讲我对实习和见习的看法,有需要有兴趣的童鞋可以看看. 半个月的见习 其实我更愿意把实习和见习分开讲,实习指的是还没签三方或者直接就是大三 ...