上篇文章是关于建造者设计模式的,今天顺便封装一个通用的 PopupWindow 来实践一下, 同时也方便以后使用 PopupWindow,本文将从下面几个方面来介绍 PopupWindow 及其封装,具体如下:
- 概述
- 常用方法
- 基本使用
- 封装 PopupWindow
- 使用封装后的PopupWindow
- 显示效果
概述
PopupWindow 表示一个弹窗,类似于 AlertDialog,相较 AlertDialog 来说 PopupWindow 使用起来更灵活,可有任意指定要显示的位置,当然能够灵活的使用必然在某一层面有所牺牲,如 PopupWindow 相较 AlertDialog 没有默认的布局,每次都得专门创建弹窗的布局,这一点来说 AlertDialog 就比较方便了,所以在开发中没有最好的解决方案,要根据具体的需求选择最合适的解决方案。
常用设置
PopupWindow 的创建,具体如下:
public PopupWindow (Context context)
public PopupWindow(View contentView)
public PopupWindow(View contentView, int width, int height)
public PopupWindow(View contentView, int width, int height, boolean focusable)
PopupWindow 的常用属性设置,具体如下:
window.setContentView(contentView);
window.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
window.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
window.setBackgroundDrawable(new ColorDrawable(Color.GRAY));
window.setOutsideTouchable(true);
window.setOnDismissListener(this);
window.setTouchable(true);
window.setAnimationStyle(R.style.PopupWindowTranslateTheme);
PopupWindow 的显示有两种设置方式,一种是基于坐标,另一种是基于某个 View ,具体如下:
void showAtLocation (View parent, int gravity, int x, int y)
void showAsDropDown (View anchor, int xoff, int yoff, int gravity)
void showAsDropDown (View anchor, int xoff, int yoff)
void showAsDropDown (View anchor)
基本使用
PopupWindow 的主要内容基本如上,下面使用原生的 PopupWindow 实现一个弹窗,下面是关键代码,具体如下:
PopupWindow window = new PopupWindow(this);
window.setContentView(contentView);
window.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
window.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
window.setBackgroundDrawable(new ColorDrawable(Color.GRAY));
window.setOutsideTouchable(true);
window.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
}
});
window.setTouchable(true);
window.setAnimationStyle(R.style.PopupWindowTranslateTheme);
window.showAtLocation(btnTarget, Gravity.BOTTOM | Gravity.CENTER, 0, 0);
下面是显示效果,具体如下:

封装 PopupWindow
这里对 PopupWindow 的封装主要是对 PopupWindow 常用摆放位置做进一步封装,使 PopupWindow 的调用更加灵活、简洁。
在封装过程中遇到的问题是不能正确获取到 PopupWindow 的宽高,正确获取宽高的方法是先对 PopupWindow 进行测量,然后再获取其宽高,具体如下:
mPopupWindow.getContentView().measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
int popupWidth = mPopupWindow.getContentView().getMeasuredWidth();
int popupHeight = mPopupWindow.getContentView().getMeasuredHeight();
对 PopupWindow 的封装使用了建造者设计模式,下面看一下 PopupWindow 的默认配置,具体如下:
public Builder(Context context) {
this.context = context;
this.popupWindow = new PopupWindow(context);
this.outsideTouchable = true;
this.touchable = true;
this.backgroundDrawable = new ColorDrawable(Color.TRANSPARENT);
this.width = WindowManager.LayoutParams.WRAP_CONTENT;
this.height = WindowManager.LayoutParams.WRAP_CONTENT;
this.gravity = Gravity.CENTER;
this.layoutId = -1;
this.offsetX = 0;
this.offsetY = 0;
}
由于宽高、背景、是否可点击等相关属性已经设置了默认值,使用时根据自己的需求设置相关属性,如 PopupWindow 的动画等,所以这些设置肯定是非必须的,那么那些事创建时必须的呢。
下面是对 PopupWindow 封装类 MPopupWindow 的初始化,具体如下:
private void setPopupWindowConfig(MPopupWindow window) {
if (contentView != null && layoutId != -1){
throw new MException("setContentView and setLayoutId can't be used together.", "0");
}else if (contentView == null && layoutId == -1){
throw new MException("contentView or layoutId can't be null.", "1");
}
if (context == null) {
throw new MException("context can't be null.", "2");
} else {
window.mContext = this.context;
}
window.mWidth = this.width;
window.mHeight = this.height;
window.mView = this.contentView;
window.mLayoutId = layoutId;
window.mPopupWindow = this.popupWindow;
window.mOutsideTouchable = this.outsideTouchable;
window.mBackgroundDrawable = this.backgroundDrawable;
window.mOnDismissListener = this.onDismissListener;
window.mAnimationStyle = this.animationStyle;
window.mTouchable = this.touchable;
window.mOffsetX = this.offsetX;
window.mOffsetY = this.offsetY;
window.mGravity = this.gravity;
}
}
显然,这里可以看出 context 和 contentView 或 layoutId 是必须需要设置的,如果没有设置相应的会有错误提示,当然在封装中也对 contentView 和 layoutId 不能同时使用做了限制和如果使用了两者的错误提示。
下面是对外提供的显示 PopupWindow 的方法,根据不同的枚举类型将 PopupWindow 显示在不同的位置,具体如下:
public void showPopupWindow(View v, LocationType type) {
if (mView!=null){
mPopupWindow.setContentView(mView);
}else if (mLayoutId != -1){
View contentView = LayoutInflater.from(mContext).inflate(mLayoutId, null);
mPopupWindow.setContentView(contentView);
}
mPopupWindow.setWidth(mWidth);
mPopupWindow.setHeight(mHeight);
mPopupWindow.setBackgroundDrawable(mBackgroundDrawable);
mPopupWindow.setOutsideTouchable(mOutsideTouchable);
mPopupWindow.setOnDismissListener(mOnDismissListener);
mPopupWindow.setAnimationStyle(mAnimationStyle);
mPopupWindow.setTouchable(mTouchable);
int[] locations = new int[2];
v.getLocationOnScreen(locations);
int left = locations[0];
int top = locations[1];
mPopupWindow.getContentView().measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
int popupWidth = mPopupWindow.getContentView().getMeasuredWidth();
int popupHeight = mPopupWindow.getContentView().getMeasuredHeight();
switch (type) {
case TOP_LEFT:
mPopupWindow.showAtLocation(v,Gravity.NO_GRAVITY,left - popupWidth + mOffsetX,top - popupHeight + mOffsetY);
break;
case TOP_CENTER:
int offsetX = (v.getWidth() - popupWidth) / 2;
mPopupWindow.showAtLocation(v,Gravity.NO_GRAVITY,left + offsetX + mOffsetX,top - popupHeight + mOffsetY);
break;
case TOP_RIGHT:
mPopupWindow.showAtLocation(v,Gravity.NO_GRAVITY,left + v.getWidth() + mOffsetX,top - popupHeight + mOffsetY);
break;
case BOTTOM_LEFT:
mPopupWindow.showAsDropDown(v, -popupWidth + mOffsetX,mOffsetY);
break;
case BOTTOM_CENTER:
int offsetX1 = (v.getWidth() - popupWidth) / 2;
mPopupWindow.showAsDropDown(v,offsetX1 + mOffsetX,mOffsetY);
break;
case BOTTOM_RIGHT:
mPopupWindow.showAsDropDown(v, v.getWidth() + mOffsetX,mOffsetY);
break;
case LEFT_TOP:
mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY, left - popupWidth + mOffsetX, top - popupHeight + mOffsetY);
break;
case LEFT_BOTTOM:
mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY, left - popupWidth + mOffsetX, top + v.getHeight() + mOffsetY);
break;
case LEFT_CENTER:
int offsetY = (v.getHeight() - popupHeight) / 2;
mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY,left - popupWidth + mOffsetX,top + offsetY + mOffsetY);
break;
case RIGHT_TOP:
mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY, left + v.getWidth() + mOffsetX,top - popupHeight + mOffsetY);
break;
case RIGHT_BOTTOM:
mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY, left + v.getWidth() + mOffsetX,top + v.getHeight() + mOffsetY);
break;
case RIGHT_CENTER:
int offsetY1 = (v.getHeight() - popupHeight) / 2;
mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY,left + v.getWidth() + mOffsetX,top + offsetY1 + mOffsetY);
break;
case FROM_BOTTOM:
mPopupWindow.showAtLocation(v,mGravity,mOffsetX,mOffsetY);
break;
}
}
使用封装后的PopupWindow
下面是使用封装后的 PopupWindow,只需四行代码就可以显示一个默认的 PopupWindow 了,具体如下:
private void showPopupWindow(MPopupWindow.LocationType type) {
MPopupWindow popupWindow = new MPopupWindow
.Builder(this)
.setLayoutId(R.layout.popup_window_layout)
.build();
popupWindow.showPopupWindow(btnTarget, type);
}
由于默认 PopupWindow 背景是透明的,建议测试时设置背景。
显示效果:
下面是 PopupWindow 在各个位置的显示,具体如下:

可以选择关注微信公众号:jzman-blog 获取最新更新,一起交流学习,公众号回复 MPopupWindow 获取源码链接。
