欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

完美解决 PopupWindow 使其点击外部不消失

程序员文章站 2022-07-02 11:37:39
...

完美解决 Android 中 PopupWindow 使其点击外部不消失

  首先贴下PopupWindow正常的创建方式:

    public static HookPopupWindow createPopupWindow(Context context,int layoutIds,int width,int height, int style){
        final View contentView = LayoutInflater.from(context).inflate(layoutIds, null, false);
        HookPopupWindow mCountryWindow = new HookPopupWindow(contentView, width, height, true);
        // 设置PopupWindow的背景
        mCountryWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        // 设置PopupWindow是否能响应外部点击事件
        mCountryWindow.setOutsideTouchable(true);
        mCountryWindow.setTouchable(true);
        // 设置PopupWindow是否能响应点击事件 两条一起使用才生效
        if(style != 0)
            mCountryWindow.setAnimationStyle(style);
        return mCountryWindow;
    }

  这是我的工具类中的一个方法, HookPopupWindow 是一个继承 PopupWindow的子类, 大家就当一个PopupWindow来看即可, 我们都知道代码中 setOutsideTouchablesetTouchable 是为了让pw(即PopupWindow, 以下均简称pw), 可以点击内部以及使其点击外部消失, 有时我们为了达到点击pw外不使其消失, 将setOutsideTouchablesetTouchable都设置为true, 但此时虽然pw不消失了, 但事件都传递到activity中了, 这肯定不是我们想要的. 看有的博客写在Activity中重写onDispatchEvent方法判断如果pw如果 showing, 直接return true将所有触摸事件拦截 , 但如果这种使pw不消失的需求多了在每个Activity岂不是很麻烦(或许可以放在基类里边, 但我偏偏不这么搞233), 但今天要说的不是这种方法, 我们可以继承PopupWindow在其子类中拦截dismiss()方法;

  之前有继承pw并在dismiss()方法打断点, 发现当点击pw外部(前提是正常方式创建–见上边创建代码), 以及点击返回键都会走dismiss()方法, 这样就简单了, 我们继承pw重写dismiss()方法并注释掉super.dismiss()点击外部就可以实现不隐藏了, 代码如下:

public class HookPopupWindow extends PopupWindow {
    public boolean canDismiss = true;// 设为false可以控制dismiss()无参方法不调用 主要是为了点击PopupWindow外部不消失

    @Override
    public void dismiss() {
//        new Exception().printStackTrace();
        if(canDismiss)
            dismiss2();
        else {
            StackTraceElement[] stackTrace = new Exception().getStackTrace();
            if(stackTrace.length >= 2 && "dispatchKeyEvent".equals(stackTrace[1].getMethodName())){
                dismiss2();
            }
        }
    }

    public void dismiss2(){
        super.dismiss();
    }
}

   在dismiss前将canDismiss置为false, 当我们需要dismiss的时候需要调用dismiss2()而不是dismiss(), 但是我在测试过程中遇到了一个问题, 就是当pw showing的时候, 我们点击返回键也是走到dismiss()方法,所以导致点back键不能dismiss, 并且Activity的onBackPrexxx onKeyDown onKeyUp 以及 popupWindow.getContentView().setOnKeyListener()都无效, 这不是我想要的, 于是我在dismiss()方法中 new Exception().printStackTrace();来打印方法堆栈, 看当点击返回键是由谁调过来的:
完美解决 PopupWindow 使其点击外部不消失
OK, 我们再来看PopupWindow 第2379 行

完美解决 PopupWindow 使其点击外部不消失

 //View中的方法
  public KeyEvent.DispatcherState getKeyDispatcherState() {
      return mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null;
  }

多少有些偏差, 其实是在第 2382 行调用的(我看到是 api 26的代码), 最初看了这段代码之后想到通过反射将 getKeyDispatcherState的返回值即 mAttachInfo.mKeyDispatchState(该变量是final类型, 可以通过反射修改)通过反射置为null , 这样事件就可以传递下去, Activity就能收到 back事件了, 但实验结果表明事件并没有分发给Activity, 而是在某个地方被处理了, super.dispatchKeyEvent();中一堆一堆的, 懒得看于是想着换个思路, 能不能不处理上述变量, 而是在dismiss()中进行拦截, 但是这样以来我怎么得知是用户点击的返回键而调用的 dismiss() 方法呢?增加一个变量? 也没地方加 看dispatchKeyEvent()方法中有没有保存修改什么变量? 貌似也没有, 正发愁的时候我看到 new Exception().printStackTrace(); 刚才打的方法堆栈, 如果我能拿到堆栈字符串, 判断其中有dispatchKeyEvent字符串不就说明是由用户点击返回键调用的吗? 于是就有了:

 StackTraceElement[] stackTrace = new Exception().getStackTrace();
   if(stackTrace.length >= 2 && "dispatchKeyEvent".equals(stackTrace[1].getMethodName())){
       dismiss2();
   }

实测可以完美解决PopupWindow点击外部使其不消失问题, 并且不影响返回键