PopupWindow 无法在点击外部后自动消失的问题

  最近在 Android 5.x 上遇到了一个 PopupWindow 无法消失的问题,PopupWindow 已经设置了

popup.setOutsideTouchable(true);
popup.setOutsideTouchable(true);
popup.setOutsideTouchable(true);

但点击 PopupWindow 外部无法消除 PopupWindow。在 Android 6.0 上没有此问题。

  该问题的原因是在使用PopupWindow时,为了取消默认的带阴影效果,使用了

popup.setBackgroundDrawable(null);
popup.setBackgroundDrawable(null);
popup.setBackgroundDrawable(null);

使用非 null 的 Background 即可解决此问题,如:

popup.setBackgroundDrawable(
new ColorDrawable(ContextCompat.getColor(this, R.color.popupBackground)));
popup.setBackgroundDrawable( new ColorDrawable(ContextCompat.getColor(this, R.color.popupBackground)));
popup.setBackgroundDrawable(
        new ColorDrawable(ContextCompat.getColor(this, R.color.popupBackground)));

  查看 Android 5.1 版本中的 PopupWindow 可以看到,在 preparePopup() 阶段,会检查 mBackground 是否为 null,若不为 null,则会生成一个 PopupViewContainer:

private void preparePopup(WindowManager.LayoutParams p) {
// ...
if (mBackground != null) {
// ...
// 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;
}
// ...
}
private void preparePopup(WindowManager.LayoutParams p) { // ... if (mBackground != null) { // ... // 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; } // ... }
private void preparePopup(WindowManager.LayoutParams p) {
    // ...
    if (mBackground != null) {
        // ...

        // 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;
    }
    // ...
}

  在同文件中可以找到 PopupViewContainer,它会在点击 PopupWindow 外部后调用 dismiss():

@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 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 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);
    }
}

  由此可见,如果 PopupWindow 的 Background 为 null,没有 PopupViewContainer,PopupWindow 就不会在点击外部时自动消失。

  Android 6.0 的代码中,在 preparePopup() 中可以找到:

private void preparePopup(WindowManager.LayoutParams p) {
// ...
// 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);
// ...
}
private void preparePopup(WindowManager.LayoutParams p) { // ... // 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); // ... }
private void preparePopup(WindowManager.LayoutParams p) {
    // ...

    // 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);
    // ...
}

点击外部取消的逻辑放到了 PopupDecorView 中,也就是上面的 mDecorView,由 createDecorView() 创建,与 mBackground 是否为 null 无关。所以在 Android 6.0 上,即使 Background 为 null,PopupWindow 依旧可以正常消失。