一起來封裝一個BasePopupWindow吧

羽翼君發表於2018-01-08

本專案GitHub:github.com/razerdp/Bas…

BasePopup 2.x更新思路分享連結:juejin.im/post/5c199c…

非常歡迎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

相關文章