上篇文章是关于建造者设计模式的,今天顺便封装一个通用的 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 的常用属性设置,具体如下:
//设置View(必须) window.setContentView(contentView); //设置宽(必须) window.setWidth(WindowManager.LayoutParams.MATCH_PARENT); //设置高(必须) window.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); //设置背景 window.setBackgroundDrawable(new ColorDrawable(Color.GRAY)); //设置PopupWindow之外的触摸事件 window.setOutsideTouchable(true); //设置PopupWindow消失的监听器 window.setOnDismissListener(this); //设置PopupWindow上的触摸事件 window.setTouchable(true); //设置PopupWindow弹出动画 window.setAnimationStyle(R.style.PopupWindowTranslateTheme);
PopupWindow 的显示有两种设置方式,一种是基于坐标,另一种是基于某个 View ,具体如下:
//基于坐标,参数(当前窗口的某个 View,位置,起始坐标x, 起始坐标y) void showAtLocation (View parent, int gravity, int x, int y) //基于某个View,参数(附着的View,x 方向的偏移量,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 PopupWindow window = new PopupWindow(this); //设置显示View window.setContentView(contentView); //设置宽高 window.setWidth(WindowManager.LayoutParams.MATCH_PARENT); window.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); //设置背景 window.setBackgroundDrawable(new ColorDrawable(Color.GRAY)); //设置PopupWindow之外的触摸事件 window.setOutsideTouchable(true); //设置PopupWindow消失的监听器 window.setOnDismissListener(new PopupWindow.OnDismissListener() { @Override public void onDismiss() { //监听PopupWindow的消失 } }); //设置PopupWindow上的触摸事件 window.setTouchable(true); //设置PopupWindow弹出动画 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); //默认PopupWindow响应触摸事件 this.outsideTouchable = true; //默认响应触摸事件 this.touchable = true; //默认背景透明 this.backgroundDrawable = new ColorDrawable(Color.TRANSPARENT); //默认宽高为WRAP_CONTENT this.width = WindowManager.LayoutParams.WRAP_CONTENT; this.height = WindowManager.LayoutParams.WRAP_CONTENT; //默认Gravity为Gravity.CENTER this.gravity = Gravity.CENTER; this.layoutId = -1; //默认偏移量为0 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); //获取目标View的坐标 int[] locations = new int[2]; v.getLocationOnScreen(locations); int left = locations[0]; int top = locations[1]; //获取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(); 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 了,具体如下:
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 获取源码链接。