淺談 PopupWindow 在 Android 開發中的使用

Android中彈出式菜單(以下稱彈窗)是使用十分廣泛一種菜單呈現的方式,彈窗爲用戶交互提供了便利。關於彈窗的實現大致有以下兩種方式AlertDialog和PopupWindow,當然網上也有使用Activity並配合Dialog主題的方式實現彈窗,有興趣的朋友也可以去研究一下。對於AlertDialog和PopupWindow兩者的最主要區別也有以下兩點:

  • 1 、位置是否固定。 AlertDialog在位置顯示上是固定的,而PopupWindow則相對比較隨意,能夠在主屏幕上的任意位置顯示。
  • 2、是否會阻塞UI線程。 AlertDialog在顯示的時候不會阻塞UI線程,而PopupWindow在顯示的時候會阻塞UI線程。

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.activity_main, 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就是表示不關閉咯! 那麼我們就動手試一下吧,驗證一下是不是和我們想象的相吻合:

setBackgroundDrawable

setFocusable setOutsideTouchable 點擊返回按鈕 點擊外部區域
colorDrawable true true 關閉窗口 關閉彈窗
colorDrawable true false 關閉窗口 關閉彈窗
colorDrawable false true 退出當前Activity 關閉彈窗
colorDrawable false false 退出當前Activity 不關閉彈窗
null true true 無操作響應 不關閉彈窗
null true false 無操作響應 不關閉彈窗
null false true 退出當前Activity 不關閉彈窗
null false false 退出當前Activity 不關閉彈窗

實驗結果似乎和我們想象的不太一樣,於是試着上網找找結果看得出如下結論: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:

  Drawable mBackgroundDrawable;
    @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的顯示大致又可以分爲兩類:相對於視圖中某個控件的相對位置(默認位於控件的正左下方)和相對於父控件的相對位置;

相對於視圖中某個控件的相對位置:

  • a,showAsDropDown(View anchor):相對某個控件的位置(正左下方),無偏移。
  • b,showAsDropDown(View anchor, int xoff, int yoff):相對某個控件的位置,同時可以設置偏移。
  • c,showAsDropDown(View anchor, int xoff, int yoff, int gravity):相對某個控件的位置,對齊方式(嘗試過,但似乎沒有效果),同時可以設置偏移。

相對於父控件的相對位置:

  • a,showAtLocation(View parent, int gravity, int x, int y):相對於父控件的位置,同時可以設置偏移量。


好了,關於PopupWindow的使用就介紹到這裏。如果文中有什麼敘述不當的地方,希望能夠指出,期待您的意見,我們一起交流。最後附上我自己嘗試去寫的BasePopupWindow基類,包含窗口出現和消失的一個背景漸變動畫,可以使窗口出現消失顯得不那麼生硬,BasePopupWindow:

public class BasePopupWindow extends PopupWindow {
    private Context mContext;
    private float mShowAlpha = 0.88f;
    private Drawable mBackgroundDrawable;
    private boolean isOutsideTouchable;

    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);
    }
}

轉載自:http://www.yidianzixun.com/n/0DQJ5UBR?s=10&appid=xiaomi&ver=3.6.2&utk=0bbfr0ip

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章