在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