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

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

popup.setOutsideTouchable(true);

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

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

popup.setBackgroundDrawable(null);

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

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

  在同文件中可以找到 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);
    }
}

  由此可见,如果 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);
    // ...
}

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