一起来封装一个BasePopupWindow吧

作者: Razerdp | 来源:发表于2016-03-05 21:50 被阅读6308次

    本项目GitHub:https://github.com/razerdp/BasePopup

    非常欢迎PR(dev分支)哦

    本文首发于CSDN,次发于泡网在简书这里发布,算是第三次修改了,这个项目也算是初步完成了,如果说要加些什么,转屏保持显示算不算一个。。。

    当然,今天写这个文章的目的是为了方便朋友圈那边文章的排版,毕竟咱们朋友圈系列只要搞朋友圈相关的好了,其他的控件一律封装到别的文集里面。


    介绍(超级简单版)

    在安卓系统,我们经常会接触到弹窗,说到弹窗,我们经常接触到的也就dialog或者popupWindow了。而这两者的区别,简单的说就是“一大小二蒙层三阻塞”,如果再简单点说,就是对话框与悬浮框的区别吧。。。具体还是谷歌咯- -这里就不详细叙述了。

    问题

    如果我们度娘过popupWindow,我们会知道,要是用一个popup,基本要以下几个步骤:

    1. 弄个布局
    2. new 一个popup(传入大小)
    3. 这个popup对象一大堆setxxxxx(特别是setBackgroundDrawable)
    4. 如果还需要动画,那么你通常会搜到的方法是。。。。xml弄出动画, style里面设定android:windowEnterAnimation和android:windowExitAnimation,然后执行第三步setXXXXX
    5. showAtLocation或者showAsDropDown什么的

    OMG!!!作为一个程序员,我想要的只是跟TextView一样,new一个对象,setText,完。做这么多东东,又是style什么的,真心想哭。

    于是,对此解决方法就是,封装吧,亲。


    封装

    首先,咱们要针对以上的问题提出一个期望的目标,很简单,new一个popup,show,完- -。

    那么为了以后的扩展,我们需要我们的popup最基本都要实现以下的功能:

    • 自由的定义样式
    • 便利的动画实现
    • 可扩展
    • 代码简洁易懂

    在开工前,我们先说说popup吧,popup支持我们添加view来将其浮在当前层上,说到底,还不是windowManger.addView,将view给弄到decorView(注意,此decorView指popup的内部类PopupDecorView,是一个FrameLayout)上,那就悬浮了嘛。。。

    popup源码

    既然如此,在安卓里面,万(可见)物基于view嘛~所以我们何不弄个ViewGroup进popup,然后我们把它当成activity的布局一样,完成各种好玩的,比如点击事件,比如动画什么的。

    于是我们的工作流程就很清楚了:

    1. 提供设置view的接口
    2. 提供设置动画方法
    3. 提供额外的辅助方法,比如点击事件什么的
    4. 统一管理showAtLocation方法

    OK,大致流程确定,接下来我们一步一步的实现它。

    Step 1 - 接口定义

    首先,定义一个interface:

    public interface BasePopup {
         View getPopupView();
         View getAnimaView();
    }
    

    该接口提供两个功能:

    • 得到popup的view(即我们需要inflate的xml)
    • 得到需要播放动画的view

    这里还有一个可以考虑,为了更加简便,我们可以考虑再添加一个方法:int getPopupViewById(),这样我们就不用在实现的时候写那么多的LayoutInflate.xxxxx了

    Step 2 - BasePopup抽象

    可以肯定的是,我们要实现各种各样的popup,那么我们肯定不能是具体类,因为具体类限制必定很多,所以我们抽象起来,至于具体的实现扔给子类完成就好了。

    public abstract class BasePopupWindow implements BasePopup {
        private static final String TAG = "BasePopupWindow";
        //元素定义
        protected PopupWindow mPopupWindow;
        //popup视图
        protected View mPopupView;
        protected View mAnimaView;
        protected View mDismissView;
        protected Activity mContext;
        //是否自动弹出输入框(default:false)
        private boolean autoShowInputMethod = false;
        private OnDismissListener mOnDismissListener;
        //anima
        protected Animation curExitAnima;
        protected Animator curExitAnimator;
        protected Animation curAnima;
        protected Animator curAnimator;
    
        public BasePopupWindow(Activity context) {
            initView(context, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        }
    
        public BasePopupWindow(Activity context, int w, int h) {
            initView(context, w, h);
        }
    }
    

    这里解释一下:因为是抽象,我们大多数的权限都给protected,在我们的变量,可以看到似乎重复了挺多的,从命名上看,我们可以分成这么几类:

    • View:
      • popup主体(即xml)
      • 需要播放动画的view
      • 点击执行dismiss的view
    • Anima,分为两种主要是因为有些特别点的效果用animator更好:
      • animation(enter/exit)
      • animator(enter/exit)
    • Other:一些配置和接口

    构造器里,我们只给出两种,一种是传入context,一种是指定宽高,这样就可以适应绝大多数的使用场景了。

    接下来我们初始化我们的view:

    private void initView(Activity context, int w, int h) {
            mContext = context;
    
            mPopupView = getPopupView();
            mPopupView.setFocusableInTouchMode(true);
            //默认占满全屏
            mPopupWindow = new PopupWindow(mPopupView, w, h);
            //指定透明背景,back键相关
            mPopupWindow.setBackgroundDrawable(new ColorDrawable());
            mPopupWindow.setFocusable(true);
            mPopupWindow.setOutsideTouchable(true);
            //无需动画
            mPopupWindow.setAnimationStyle(0);
    
            //=============================================================为外层的view添加点击事件,并设置点击消失
            mAnimaView = getAnimaView();
            mDismissView = getClickToDismissView();
            if (mDismissView != null) {
                mDismissView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        dismiss();
                    }
                });
                if (mAnimaView != null) {
                    mAnimaView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
    
                        }
                    });
                }
            }
            //=============================================================元素获取
            curAnima = getShowAnimation();
            curAnimator = getShowAnimator();
            curExitAnima = getExitAnimation();
            curExitAnimator = getExitAnimator();
        }
    

    在初始化方法里,我们主要是初始化一些常见的配置参数,但要注意的是,我们的view是在popup new出来之前就获取好的,当然,是通过抽象方法给子类实现。至于为什么mAnimaView 要给个点击事件但不实现呢,这里主要是防止点击事件被屏蔽了。

    我们可以看到各种getXXXX,在之前的版本中我给定全部都是抽象方法,后来发现,没这个必要,于是这些方法只保留了几个抽象的,其他的都是功用方法(应该改为protected?)

        protected abstract Animation getShowAnimation();
    
        protected abstract View getClickToDismissView();
    
        public Animator getShowAnimator() { return null; }
    
        public View getInputView() { return null; }
    
        public Animation getExitAnimation() {
            return null;
        }
    
        public Animator getExitAnimator() {
            return null;
        }
    

    接下来是showPopup,这里提供三个方法,分别是无参/紫苑id/view

    showPopup

    这三个方法都指向于同一个方法:tryToShowPopup

    private void tryToShowPopup(int res, View v) throws Exception {
            //传递了view
            if (res == 0 && v != null) {
                mPopupWindow.showAtLocation(v, Gravity.CENTER, 0, 0);
            }
            //传递了res
            if (res != 0 && v == null) {
                mPopupWindow.showAtLocation(mContext.findViewById(res), Gravity.CENTER, 0, 0);
            }
            //什么都没传递,取顶级view的id
            if (res == 0 && v == null) {
                mPopupWindow.showAtLocation(mContext.findViewById(android.R.id.content), Gravity.CENTER, 0, 0);
            }
            if (curAnima != null && mAnimaView != null) {
                mAnimaView.clearAnimation();
                mAnimaView.startAnimation(curAnima);
            }
            if (curAnima == null && curAnimator != null && mAnimaView != null) {
                curAnimator.start();
            }
            //自动弹出键盘
            if (autoShowInputMethod && getInputView() != null) {
                getInputView().requestFocus();
                InputMethodUtils.showInputMethod(getInputView(), 150);
            }
        }
    

    相关的注释也写了,其中android.R.id.content是decorView的contnet的id,也就是我们setContentView的父类id。

    接下来我们需要对一些状态操作进行控制,比如dismiss:

     public void dismiss() {
            try {
                if (curExitAnima != null) {
                    curExitAnima.setAnimationListener(mAnimationListener);
                    mAnimaView.clearAnimation();
                    mAnimaView.startAnimation(curExitAnima);
                }
                else if (curExitAnimator != null) {
                    curExitAnimator.removeListener(mAnimatorListener);
                    curExitAnimator.addListener(mAnimatorListener);
                    curExitAnimator.start();
                }
                else {
                    mPopupWindow.dismiss();
                }
            } catch (Exception e) {
                Log.d(TAG, "dismiss error");
            }
        }
    

    如果存在exit animation/animator,则在dismiss前播放,当然,我们的anima需要给定监听器:

      private Animator.AnimatorListener mAnimatorListener = new Animator.AnimatorListener() {
        ...animatorstart
    
            @Override
            public void onAnimationEnd(Animator animation) {
                mPopupWindow.dismiss();
            }
    
      ...animator cancel
      ...animator repeat
        };
    
        private Animation.AnimationListener mAnimationListener = new Animation.AnimationListener() {
         ...animationstart
            @Override
            public void onAnimationEnd(Animation animation) {
                mPopupWindow.dismiss();
            }
        ...animation repeat
        };
    

    这样就可以确保我们在执行完动画才去dismiss

    这样,我们的basepopup就封装好了,以后子类继承他仅仅需要实现四个方法,然后就可以跟平时写布局一样使用popup了(甚至getClickToDismissView也可以不用管,如果不是需要点击消失的话)

    例子

    下面是一些根据这个basepopup写的例子(具体的可以到github看,而图一,将会是接下来为朋友圈点赞控件实现的效果):

    comment_popup_with_exitAnima.gif
    dialog_popup.gif
    input_popup.gif
    list_popup.gif
    menu_popup.gif
    scale_popup.gif
    slide_from_bottom_popup.gif

    相关文章

      网友评论

      • a9c753ef15ef:在吗,大兄弟,我就想知道怎么在你的popupwindow里startActivityForResult
      • db6b457199dd:带有输入框的pop在弹出时,软键盘不跟着弹出来?
      • 7e625c8993c4:你的这个库,第一次点击popupwindow之外的地方不响应点击事件,怎么解决??
        7e625c8993c4:@羽翼君 那点击 popup之外的控件,只有等当前的popup消失了,之外的控件才能响应点击事件,所以需要点2次,有啥办法可以在点击外部控件的时候,当前popuo消失,并且可以响应外部控件的点击事件
        Razerdp:你好,如果可以的话,贴上demo到issue

        另外,popup本身就是阻塞点击事件的,触摸反馈会优先被popup消化
      • 430cbe953065:有几个拼写错误的,LZ可以修改一下~:smile: ~~不过还是写的很漂亮!
        Razerdp:@Chenley 哇!!! 大兄弟,你好仔细呀~棒-V-

        你说的问题我都没怎么发现呢哈哈。。。我都佩服你看得那么仔细了

        其实关于方法和注释问题,我曾经回答过,方法名因为一开始是为自己用的,所以有些方法和变量可以说是乱来的哈哈,后来star数和使用的人多了,我才开始重构并重视这个问题

        然后注释,,,唔,也是一样,曾经写过一份英文注释,只是仅仅过了四六级的我。。。现在 的英语水平,噗-V- (注释这里我尽量切换为中文吧)

        最后,非常感谢你的指出,真的好厉害诶,如果可以的话,欢迎你提交pullrequest~让更多人看到你的付出怎么样-V-
        430cbe953065:@羽翼君
        1、private int popupLayoutid;(建议驼峰哦,普通变量建议m开头通用的写法呢)
        2、 int offset[]; (通常数据是 int[] offset;的写法呢)
        3、boolean activityValided; 应该是availabled吧
        4、private int[] calcuateOffset(View anchorView);是不是calculateOffset?
        5、public BasePopupWindow setDismissWhenTouchOuside(boolean dismissWhenTouchOuside) :outside
        6、 * <b>return ture for perform dismiss</b> :好像好几个true的注释都有点问题;
        目前看到了这些~~LZ可以去看看~~另外还是向lz学习多开源的~~:blush:
        Razerdp:哪几个呢~
      • d5bff03f6c05:在6.0以上,拦截返回键有没有相应的处理?
      • 街道shu记:可以给个apk下载地址啊
      • am_skyf:请问下,用这个怎么让popupwindow显示在控件的上方呢?为什么根据setOffsetX
        setOffsetY 调整位置没反应,
      • 051db9556400:嗨~我在Android7.0上发现,弹窗在View下面的时候没有显示。
        Yellow158:这是android 7.0的bug,下载lz的代码导入自己的项目,然后参照下面链接的文章进行修改
        修改lz的BasePopupWindow.java下的tryToShowPopup
        http://blog.csdn.net/ithe1001/article/details/56281750
      • AIllll:感觉github上面那些方法的说明 有些我不是太能理解 比如getClickToDismissView():点击触发dismiss的view ,啥叫点击触发dismiss。。。
        Razerdp:@AIllll gravity使用跟showAtLocation的gravity一样的。其取值传递到popup.showAtLocation,具体而言是相对于父控件的位置(不是相对于anchorView哦),一般情况下看作相对于decorView的gravity位置就好了。
        AIllll:@羽翼君 setPopupGravity,参数应该给什么,没找到相关说明,我给的Gravity.TOP,但是没用
        Razerdp:@AIllll 其实在ReadMe我也写过这个问题,初期的时候随便写的名字(个人使用),后来抽出来后除了写过几篇文章后也没怎么宣传过,然后现在有几百的热度(虽然不是很多),上一次3.0修改了部分方法名后就有人跟我说更新了库之后要改好多方法(可能他们工程使用很多了)……


        于是我就不太敢随意改方法名了…


        谢谢你的反馈,这个问题其实我也意识到了我的错误,以后会注意的。


        另外,这个getClickToDismiss初衷其实是想说指定一个点击关闭的VIew的,初期没怎么考虑就直接按意思直译过来了,我的锅,sry
      • f3ede8b44fb7:有个缺点 就是我的类本来就要继承一个base类 那你这个就用不了了 好心疼啊
      • fendo:赞一个!
      • c28098ec0d93:太好了,大赞👍
      • 85e93634662f:必须收藏!
      • 阿飞咯:学习了
      • zyyoona7:太赞了,收藏
      • c3302fe6d5e9:ContentView 如何替换为Fragment,期待新版本满足更多功能 :+1:
        Razerdp:@赵小布 popupwindow是无法把fragment弄进去的说
      • 灯火斑斓_Ljh:非常不错
      • 虾米小华:想问一下你的动态图怎么做的 :smiley:
        虾米小华:谢谢,做得很用心:)
        Razerdp:@虾米小华 如果是电脑PC,建议用LICEcap(要保证存储gif的目录跟LICEcapq同一个目录哦)
        Razerdp:@虾米小华 我是用AS录制成MP4(它躲在AndroidMonitor最左边截图按钮下面那个绿色的播放按钮),然后用Video to Gif这个软件转成GIF
      • 208f5ab89dba:挺不错的:+1:
      • 陆地蛟龙:你真高产,后生可畏。
        Razerdp:@胡髭蛤蟆 木有高产……隔了好久才贴一篇的说
      • a7eb9c8e544c:感谢分享,很实用
      • Alex_Cin:怒赞啊,刚看到!!
      • Alex_Cin:怒赞啊,刚看到!!
      • 空心菜的爱:这么好的文章竟然没评论?

      本文标题:一起来封装一个BasePopupWindow吧

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