一起来封装一个BasePopupWindow吧

本项目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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容