浅谈PopupWindow在Android开发中的使用

作者: 采蘑菇的里奥马 | 来源:发表于2016-05-10 11:54 被阅读40779次
腾讯微信

在Android中弹出式菜单(以下称弹窗)是使用十分广泛一种菜单呈现的方式,弹窗为用户交互提供了便利。关于弹窗的实现大致有以下两种方式AlertDialog和PopupWindow,当然网上也有使用Activity并配合Dialog主题的方式实现弹窗,有兴趣的朋友也可以去研究一下。对于AlertDialog和PopupWindow两者的最主要区别就是显示的位置问题:

  • 位置是否固定。 AlertDialog在位置显示上是固定的,而PopupWindow则相对比较随意,能够在主屏幕上的任意位置显示。

PopupWindow在android.widget包下,Google官方文档对PopupWindow的描述是:

"A popup window that can be used to display an arbitrary view. The popupwindow is a floating container that appears on top of the current activity."

也就是说PopupWindow是一个以弹窗方式呈现的控件,可以用来显示任意视图(View),而且会浮动在当前活动(activity)的顶部”。因此我们可以通过PopupWindow实现各种各样的弹窗效果,进行信息的展示或者是UI交互,由于PopupWindow自定义布局比较方便,而且在显示位置比较自由不受限制,因此受到众多开发者的青睐。

废话不多说,进入正题。

PopupWindow的使用

其实PopupWindow的使用非常简单,总的来说分为两步:

  • 1、调用PopupWindow的构造器创建PopupWindow对象,并完成一些初始化设置。
  • 2、调用PopupWindow的showAsDropDown(View view)将PopupWindow作为View组件的下拉组件显示出来;或调用PopupWindow的showAtLocation()方法将PopupWindow在指定位置显示出来。
创建并完成初始化设置:
PopupWindow popupWindow = new PopupWindow(this);
popupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
popupWindow.setContentView(LayoutInflater.from(this).inflate(R.layout.layout_popupwindow_style01, null));
popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));
popupWindow.setOutsideTouchable(false);
popupWindow.setFocusable(true);

其中,setWidth、setHeight和setContentView三者必须实现,否则将不会显示任何视图。 setwidth和setHeight的参数值可以是具体的数值,也可以是MATCH_PARENT或者是WRAP_CONTENT。setContentView则是为PopupWindow设置视图内容。 setFocusable顾名思义就是让PopupWindow获得焦点。 setBackgroundDrawable从字面理解就是为PopupWindow设置一个背景。 setOutsideTouchable则表示PopupWindow内容区域外的区域是否响应点击事件,Android官方给出的文档则表示点击内容区域外的区域是否关闭窗口,那么设置为true应该就是表示关闭,设置为false就是表示不关闭咯! 那么我们就动手试一下吧,验证一下是不是和我们想象的相吻合:

QQ截图

实验结果似乎和我们想象的不太一样,于是试着上网找找结果看得出如下结论:setFocusable确实是让PopupWindow获得焦点,获得焦点的PopupWindow能够处理物理按钮的点击事件,否则点击事件将向上传递由Activity处理,这也能够解释为什么在setFocusable(false)的情况下点击返回按钮会退出当前Activity。关于焦点设置需要注意的一点是:****如果PopupWindow中有Editor的话,focusable必须要为true。****

可是还是有一点我似乎不太明白,为什么设置setOutsideTouchable(true),点击外部区域还是不会关闭窗口呢,这似乎与Google官方给出的解释有点出入,于是试着从源码寻找答案:

源码截图

不看不知道,原来另有玄机,外部点击事件的响应还backgroundDrawable有关,在backgroundDrawable!=null的情况下,PopupWindow会以backgroundDrawable作为背景生成一个根据contentView和backgroundDrawable生成一个PopupBackgroundView并返回,而如果在backgroundDrawable==null的情况下,则直接返回contentView。于是乎接着往下搜索,原来PopupBackgroundView是一个内部私有类继承至FrameLayout,且该类完成了对onKey和onTouch事件的分发处理。因为contentView我们并没有进行onKey和onTouch事件的分发处理,所以在backgroundDrawable!=null的情况下,即使PopupWindow获得屏幕焦点,PopupWindow也不能处理物理按键的点击事件,因此就算点击返回按钮也会没有任何反应,更别说外部点击事件的响应了。这样也就能解释为什么在backgroundDrawable!=null的情况下点击返回键或者是点击外部区域都不会关闭窗口了,因此我们在使用PopupWindow的时候都会设置焦点并且再设置一个背景,但为了不影响显示效果可以设置一个全透明背景:

 setBackgroundDrawable(new ColorDrawable(0x00000000));

但是,眼尖的朋友已经发现,还有一种情况似乎解释不通在setBackgroundDrawable(new ColorDrawable(0x00000000))、setOutsideTouchable(false)、setFocusable(true)的情况下,话说背景也有了,也设置焦点了,为什么setOutsideTouchable(false)点击外部区域还是会关闭窗口呢? 说实话看了半天源码也没能明白个啥意思,好吧,这可能也是Android的一个Bug,只能想办法去解决,于是我想着是否可以重写setOutsideTouchable方法和setContentView方法来解决问题呢。在setContentView的时候,使contentView获得焦点并添加按键监听事件,于是在任何情况下都能响应返回按钮的点击事件了。在setOutsideTouchable的时候判断为true的话就设置PopupWindow的backgroundDrawable,如果backgroundDrawable==null的话,就新建一个透明背景,也不影响显示效果。

重写setContentView():

@Override
public void setContentView(View contentView) {
    if(contentView != null) {
        super.setContentView(contentView);
        contentView.setFocusable(true);   //
        contentView.setFocusableInTouchMode(true);   //
        contentView.setOnKeyListener(new View.OnKeyListener() {
            
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                switch (keyCode) {
                    case KeyEvent.KEYCODE_BACK:
                        dismiss();
                        return true;
                    default:
                        break;
                }
                return false;
            }
        });
    }
}

重写setOutsideTouchable():

@Override
public void setOutsideTouchable(boolean touchable) {
    super.setOutsideTouchable(touchable);
    if(touchable) {
        if(mBackgroundDrawable == null) {
            mBackgroundDrawable = new ColorDrawable(0x00000000);
        }
        super.setBackgroundDrawable(mBackgroundDrawable);
    } else {
        super.setBackgroundDrawable(null);
    }
}

尝试着运行一遍, Bingo! 完美解决!!!

PopupWindow的显示:

PopupWindow的显示大致又可以分为两类:相对于视图中某个控件的相对位置(默认位于控件的正左下方)和相对于父控件的相对位置;

相对于视图中某个控件的相对位置:

  • showAsDropDown(View anchor):相对某个控件的位置(正左下方),无偏移。
  • showAsDropDown(View anchor, int xoff, int yoff):相对某个控件的位置,同时可以设置偏移。
  • showAsDropDown(View anchor, int xoff, int yoff, int gravity):相对某个控件的位置,对齐方式(尝试过,但似乎没有效果),同时可以设置偏移。

相对于父控件的相对位置:

  • showAtLocation(View parent, int gravity, int x, int y):相对于父控件的位置,同时可以设置偏移量。
setOutsideTouchable.gif

好了,关于PopupWindow的使用就介绍到这里。如果文中有什么叙述不当的地方,希望能够指出,期待您的意见,我们一起交流。最后附上我自己尝试去写的BasePopupWindow基类,包含窗口出现和消失的一个背景渐变动画,可以使窗口出现消失显得不那么生硬,BasePopupWindow:

public class BasePopupWindow extends PopupWindow {

    private Context mContext;
    private float mShowAlpha = 0.88f;
    private Drawable mBackgroundDrawable;

    public BasePopupWindow(Context context) {
        this.mContext = context;
        initBasePopupWindow();
    }

    @Override
    public void setOutsideTouchable(boolean touchable) {
        super.setOutsideTouchable(touchable);
        if(touchable) {
            if(mBackgroundDrawable == null) {
                mBackgroundDrawable = new ColorDrawable(0x00000000);
            }
            super.setBackgroundDrawable(mBackgroundDrawable);
        } else {
            super.setBackgroundDrawable(null);
        }
    }

    @Override
    public void setBackgroundDrawable(Drawable background) {
        mBackgroundDrawable = background;
        setOutsideTouchable(isOutsideTouchable());
    }

    /**
     * 初始化BasePopupWindow的一些信息
     * */
    private void initBasePopupWindow() {
        setAnimationStyle(android.R.style.Animation_Dialog);
        setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
        setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
        setOutsideTouchable(true);  //默认设置outside点击无响应
        setFocusable(true);
    }

    @Override
    public void setContentView(View contentView) {
        if(contentView != null) {
            contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
            super.setContentView(contentView);
            addKeyListener(contentView);
        }
    }

    public Context getContext() {
        return mContext;
    }

    @Override
    public void showAtLocation(View parent, int gravity, int x, int y) {
        super.showAtLocation(parent, gravity, x, y);
        showAnimator().start();
    }

    @Override
    public void showAsDropDown(View anchor) {
        super.showAsDropDown(anchor);
        showAnimator().start();
    }

    @Override
    public void showAsDropDown(View anchor, int xoff, int yoff) {
        super.showAsDropDown(anchor, xoff, yoff);
        showAnimator().start();
    }

    @Override
    public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        super.showAsDropDown(anchor, xoff, yoff, gravity);
        showAnimator().start();
    }

    @Override
    public void dismiss() {
        super.dismiss();
        dismissAnimator().start();
    }

    /**
     * 窗口显示,窗口背景透明度渐变动画
     * */
    private ValueAnimator showAnimator() {
        ValueAnimator animator = ValueAnimator.ofFloat(1.0f, mShowAlpha);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float alpha = (float) animation.getAnimatedValue();
                setWindowBackgroundAlpha(alpha);
            }
        });
        animator.setDuration(360);
        return animator;
    }

    /**
     * 窗口隐藏,窗口背景透明度渐变动画
     * */
    private ValueAnimator dismissAnimator() {
        ValueAnimator animator = ValueAnimator.ofFloat(mShowAlpha, 1.0f);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float alpha = (float) animation.getAnimatedValue();
                setWindowBackgroundAlpha(alpha);
            }
        });
        animator.setDuration(320);
        return animator;
    }

    /**
     * 为窗体添加outside点击事件
     * */
    private void addKeyListener(View contentView) {
        if(contentView != null) {
            contentView.setFocusable(true);
            contentView.setFocusableInTouchMode(true);
            contentView.setOnKeyListener(new View.OnKeyListener() {

                @Override
                public boolean onKey(View view, int keyCode, KeyEvent event) {
                    switch (keyCode) {
                        case KeyEvent.KEYCODE_BACK:
                            dismiss();
                            return true;
                        default:
                            break;
                    }
                    return false;
                }
            });
        }
    }

    /**
     * 控制窗口背景的不透明度
     * */
    private void setWindowBackgroundAlpha(float alpha) {
        Window window = ((Activity)getContext()).getWindow();
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        layoutParams.alpha = alpha;
        window.setAttributes(layoutParams);
    }
}

相关文章

网友评论

  • mimimomo:如果哪个小朋友把父activity设置成dialog的样式。然后。完蛋。出现一个现象。popupwindow会从dialog的activity里面显示。如果我的pop本来是在屏幕最上方,就会变成在窗口内部的上方了
  • luoxiangcheng:4.4系统有效,7.0系统无效,用你的这个BasePopupWindow设置setOutsideTouchable(false)后,点击外部区域仍旧会关闭弹窗
  • 76cb7db34cc6:怎么样才能确认SDK的源码? 新手找了半天没找到。
  • 6c2426f9b9c3:我是不是个不合格的程序员每次看到几百行甚至上千行的源码就没耐心去分析了。
  • 上瘾_4fb2:老哥 有没有源码
  • Vander丶:博主这里面是不是少了个方法

    isOutsideTouchable() 这是个抽象方法 是吧.
  • ImTudou:可以的,大兄弟
  • kirito0424:"所以在backgroundDrawable!=null的情况下,即使PopupWindow获得屏幕焦点,PopupWindow也不能处理物理按键的点击事件,因此就算点击返回按钮也会没有任何反应,更别说外部点击事件的响应了。"这句是不是写错了呀,应该是“所以在backgroundDrawable=null的情况下” 吧?
  • sakurajiang:因为contentView我们并没有进行onKey和onTouch事件的分发处理,所以在backgroundDrawable!=null的情况下,即使PopupWindow获得屏幕焦点,PopupWindow也不能处理物理按键的点击事件,因此就算点击返回按钮也会没有任何反应,更别说外部点击事件的响应了。这样也就能解释为什么在backgroundDrawable!=null的情况下点击返回键或者是点击外部区域都不会关闭窗口了


    确定这里没写错?不应该是因为PopupView本身并没有重写Key和Touch事件的处理,所以如果没有包这个外层容器类,点击Back键或者外部区域是不会导致弹框消失的。也就是说backgroundDrawable!=null时,点击Back键或者外部区域是会导致弹框消失的
  • RoyAlex:在fragment中使用的时候,整个activity的透明度都变了。
    采蘑菇的里奥马: @RoyAlex 好的 这个问题我会根据你的情况去测试一下 !
    RoyAlex:tablayout + fragment 中使用的。fragment中有使用高德的mapview,window弹出后,整个activity的透明度都变了,可以从tablayout和toolbar看到整个alpha变化的过程
    采蘑菇的里奥马:您好! 能方便说明一下您使用的一个布局环境吗 ? 看到您这个问题 我有特异去写了一下 发现问题并不存在 .
  • 黄光华:“而PopupWindow在显示的时候会阻塞UI线程”
    应该没有阻塞吧,不然PopupWindow怎么响应点击事件?除非PopupWindow并不是在UI线程,但这不可能。
    采蘑菇的里奥马:关于这个问题也是我本人一个误解, 人云亦云, 并没有很好的实践, 多谢指出, 内容现已改正 .
  • 无名小北:难道我恩是没看明白一点源码?PopupBackgroundView是一个内部私有类继承至FrameLayout(这个没错!)且该类完成了对onKey和onTouch事件的分发处理(没看明白是哪里?),怎么感觉明明是PopupDecordView做了对onKey和onTouch事件的分发处理!!!就哪怕你说的这个是对的,我又想请问,即使我没设置backgrounddrawable,那根据事件分发原则,也会从decordview里面分发事件到其包含的子对象中,也不应该发生不响应的问题吧?还请再次详细说明!
    采蘑菇的里奥马:@无名小北 谢谢支持 我会尝试组织好语言写更好的文章 !! :grin:
    无名小北:@无名小北 不好意思!刚刚脑壳卡了一下!原来是我看错代码了!通了!只是作者可以描述再清晰一点,这样免得后人绕一个弯弯!!!任然给你一个mark!
  • a05d16383d1e:感谢,解决我的问题 :smile:
    采蘑菇的里奥马:@北极熊的物语 希望能够有所帮助 哈哈 ~
  • e33fc662e094:请问使用你这个basepop封装类 外部应该如何使用
    e33fc662e094:@Liuuuuu 谢谢 解决了
    e33fc662e094:@码农小阿飞 我看到你这里面已经写了 宽高 背景 和 点击方法, 我在外部再设置一次会产生作用么? 目前我用你这自定义的popup产生了一点问题. 点击后程序崩溃 提示

    Process: phone.example.com.a36kr, PID: 12115
    java.lang.ClassCastException: phone.example.com.a36kr.tools.MyApp cannot be cast to android.app.Activity
    at phone.example.com.a36kr.other.SecondPopupWindow.setWindowBackgroundAlpha(SecondPopupWindow.java:169)
    at phone.example.com.a36kr.other.SecondPopupWindow.access$000(SecondPopupWindow.java:21)
    at phone.example.com.a36kr.other.SecondPopupWindow$1.onAnimationUpdate(SecondPopupWindow.java:117)

    然后定位到了setWindowBackgroundAlpha 这个方法的第一行,也就是

    Window window = ((Activity)getContext()).getWindow(); 这一行

    请问与getContext有关么? 这里可以写成别的么?
    或者有什么好的解决思路提供下.
    采蘑菇的里奥马:@Liuuuuu 就和PopupWindow的用法一样的!
  • 8f1db6323755: // 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);
    23以上的似乎没有这个问题,因为对事件的响应已经迁移到了PopupDecorView中去了。
    无名小北:@8f1db6323755 问题忍让存在!如果没有多的那一层framlayout,事件依然无法传递!
    采蘑菇的里奥马:@8f1db6323755 是这么一个情况吗? 多谢建议哦,回头我会去试试看的!
  • 2a5276452da4:试了一下,感觉 `BasePopupWindow` 中,除了 `setOutsideTouchable()`方法和 `initBasePopupWindow()` 起到作用之外,其他方法似乎没什么作用,注释掉之后,效果不变。
    在 `initBasePopupWindow()` 方法中,已经设置了动画 —— `setAnimationStyle(android.R.style.Animation_Dialog);`。
  • e1c0d26bd8e8:请问如何验证“PopupWindow在显示的时候会阻塞UI线程“?我点击按钮显示PopupWindow之后立刻执行runOnUiThread函数打印一个语句,语句可以被立刻显示出来,UI线程应该没被阻塞吧?
    采蘑菇的里奥马:关于这个问题也是我本人一个误解, 人云亦云, 并没有很好的实践, 多谢指出, 内容现已改正 .
  • 47806e86e1bd:以前遇到点击的问题,自己糊里糊涂的解决了,但是没看源码总结,现在不觉明厉 :+1:
    采蘑菇的里奥马:@一路迷失Az 哈哈,很高兴能够解决你的问题。
  • Marno:很少使用popupWindow,主要目前还没有遇到什么场景是dialog解决不了的
    采蘑菇的里奥马:@雁归来兮 怎么说呢,感觉并UI非线层上的阻塞,比如在布局里设置一个progressBar,其实他还在转的。 我的理解可能是PopupWindow抢占了焦点的缘故吧,光标才会停止不动 ! :grin:
    雁归来兮:@YoungHong1992 Dialog是非阻塞线程的,而PopupWindow是阻塞线程的。So ....
    ea4b9fb017ef:@Marno Activity中建立一个EditText,该EditText获取焦点的时候光标会一直闪动。
    弹出dialog,光标仍然闪动。
    弹出popupWindow ,光标停留在弹出的一瞬间不再闪动
  • 爱孔孟:不明觉厉
    采蘑菇的里奥马:@爱孔孟 哈哈! 多多支持!
  • 536a50f65c73:很不错,有点建议就是常量按照命名规范不然很容易误导别人和自己。果当作封装类应该把宽高暴露出去,因为这是很不确定的因素。
    采蘑菇的里奥马:@OUT曼 非常感谢你的建议,变量名已经成习惯,之后文章会注意!:grin:
  • fe2a5bcdf6ea:不太懂 是不是想让点击外面不关闭按钮 必须设置一个drawable背景啊?
    采蘑菇的里奥马:@蹦蹦跳跳真可爱 慢慢学,不急得,我也是个菜鸟。 :grin:
    fe2a5bcdf6ea:@码农小阿飞 好棒 是这样啊 谢谢你这么耐心的回答 我是个菜
    采蘑菇的里奥马:@蹦蹦跳跳真可爱 是这样子的,PopupWindow的话,就正如文中的几组实验结果来看,不设置BackgroundDrawable的PopupWindow就会像僵尸窗口一样,不响应返回键的效果,而设置BackgroundDrawable的PopupWindow除非不获得焦点否则setOutsideTouchable根本不起作用,不管设置与否点击窗口外部都会关闭窗口,这样就有点不适合部分的开发需求,而文中我写的BasePopupWindow这个基类,就是对PopupWindow就是对原有PopupWindow的一个小小的封装,重写setContentView和setOutsideTouchable两个方法,使setOutsideTouchable方法的效果能够正常使用,而返回键不管BackgroundDrawable设置与否都能实现,在一个就是我个人增加一个小小的显示和隐藏的过渡动画效果,希望这一番解释你能够理解,其实可以试着自己去试试,这样更有助于加深理解。 谢谢支持!!

本文标题:浅谈PopupWindow在Android开发中的使用

本文链接:https://www.haomeiwen.com/subject/lgderttx.html