前言
在日常的开发中,经常会有弹框的操作。实现弹框有两种选,PopupWindow或者Dialog,这里就先忽略Dialog。弹框可能会在各种位置出现,在指定View的上、下、左、右、左对齐、右对齐等...
而PopupWindow似乎就提供了showAsDropDown方法(请忽略showAtLocation,这边说的是相对于View显示),这~~就有点尴尬了。
PopupWindow
平时我们可能是这样用PopupWindow的:
创建一个布局,再创建一个类继承PopupWindow
public class TestPopupWindow extends PopupWindow { public TestPopupWindow(Context context) { super(context); setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); setOutsideTouchable(true); setFocusable(true); setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); View contentView = LayoutInflater.from(context).inflate(R.layout.popup_test, null, false); setContentView(contentView); } }
然后直接使用它
TestPopupWindow mWindow = new TestPopupWindow(this); //根据指定View定位 PopupWindowCompat.showAsDropDown(mWindow, mButtom, 0, 0, Gravity.START); //或者 mWindow.showAsDropDown(...); //又或者使用showAtLocation根据屏幕来定位 mWindow.showAtLocation(...);
Gravity.LEFT(Gravity.START):相对于View左对齐;
Gravity.RIGHT(Gravity.END):相对于View靠右显示。
Gravity.CENTER:在showAsDropDown()中是跟 Gravity.LEFT一样,在showAtLocation()中Gravity.CENTER才有效果
得到效果
left.gif
查了下showAsDropDown(),发现只能在指定控件的下面弹出,总感觉少了点什么~~
有时候我想弹在View的上面、左边、右边?怎么解?
尬
可能有机智的boy已经想到了showAsDropDown()中的另外两个参数,xoff、yoff。
要利用这两个参数,不过不建议在代码中直接写。为什么?
如果你的PopupWindow宽高不确定,这两个参数你也不知道该写多少。
什么!你的PopupWindow宽高都写死了?骚年,你还是太年轻了。当你宽高确定的时候,如果PopupWindow中有文本,用户把字体改得超大,你的字就没掉一块了
什么!你还是嘴硬非要把宽高写死?好吧,你赢了。
各种位置的弹窗
下面就来利用xoff、yoff在你想要的任何位置弹框。
准备工作
弹框前,需要得到PopupWindow的大小(也就是PopupWindow中contentView的大小)。
由于contentView还未绘制,这时候的width、height都是0。因此需要通过measure测量出contentView的大小,才能进行计算。需要如下方法:
@SuppressWarnings("ResourceType") private static int makeDropDownMeasureSpec(int measureSpec) { int mode; if (measureSpec == ViewGroup.LayoutParams.WRAP_CONTENT) { mode = View.MeasureSpec.UNSPECIFIED; } else { mode = View.MeasureSpec.EXACTLY; } return View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.getSize(measureSpec), mode); }
测量contentView的大小
TestPopupWindow window = new TestPopupWindow(this); View contentView = window.getContentView(); //需要先测量,PopupWindow还未弹出时,宽高为0 contentView.measure(makeDropDownMeasureSpec(window.getWidth()), makeDropDownMeasureSpec(window.getHeight()));
弹框
测量好PopupWindow大小后,就在任意位置弹窗了
弹框的位置无非就是根据PopupWindow以及指定View的大小,计算水平、竖直方向偏移。
水平:居左;竖直:居下
计算偏移:
水平:居左;竖直:居下
代码、效果:
int offsetX = -window.getContentView().getMeasuredWidth(); int offsetY = 0; PopupWindowCompat.showAsDropDown(window, mButton, offsetX, offsetY, Gravity.START);
效果(水平:居左;竖直:居下)
水平:居中;竖直:居下
计算偏移:
水平:居中;竖直:居下
代码、效果:
offsetX = Math.abs(mWindow.getContentView().getMeasuredWidth()-mButton.getWidth()) / 2; offsetY = 0; PopupWindowCompat.showAsDropDown(window, mButton, offsetX, offsetY, Gravity.START);
效果(水平:居中;竖直:居下)
水平:右对齐;竖直:居下
计算偏移:
水平:右对齐;竖直:居下
代码、效果:
offsetX = mButton.getWidth() - mWindow.getContentView().getMeasuredWidth(); offsetY = 0; PopupWindowCompat.showAsDropDown(window, mButton, offsetX, offsetY, Gravity.START);
效果(水平:右对齐;竖直:居下)
水平:居中;竖直:居上
计算偏移:
水平:居中;竖直:居上
代码、效果:
offsetX = Math.abs(mWindow.getContentView().getMeasuredWidth()-mButton.getWidth()) / 2; offsetY = -(mWindow.getContentView().getMeasuredHeight()+mButton.getHeight()); PopupWindowCompat.showAsDropDown(window, mButton, offsetX, offsetY, Gravity.START);
效果(水平:居中;竖直:居上)
水平:居左;竖直:居中
计算偏移:
水平:居左;竖直:居中
代码、效果:
offsetX = -mWindow.getContentView().getMeasuredWidth(); offsetY = -(mWindow.getContentView().getMeasuredHeight() + mButton.getHeight()) / 2; PopupWindowCompat.showAsDropDown(window, mButton, offsetX, offsetY, Gravity.START);
效果(水平:居左;竖直:居中)
水平:居右;竖直:居中
计算偏移:
水平:居右;竖直:居中
代码、效果:
offsetX = 0; offsetY = -(mWindow.getContentView().getMeasuredHeight() + mButton.getHeight()) / 2; PopupWindowCompat.showAsDropDown(window, mButton, offsetX, offsetY, Gravity.END);
center-right.gif
画这些图比敲代码还累~~~
基本上完成了所有位置的弹框。还有一些位置上面没提到,不过通过上面那些水平、竖直的偏移也能拼凑出来。
背景变暗
说完位置方案,顺便提下背景色的变化方案。
通改变Window的透明度来实现背景变暗是常用的一种做法。
在PopupWindow中,先写个修改Window透明度的方法(注意,这边的mContext必须是Activity)
/** * 控制窗口背景的不透明度 */ private void setWindowBackgroundAlpha(float alpha) { if (mContext == null) return; if (mContext instanceof Activity) { Window window = ((Activity) mContext).getWindow(); WindowManager.LayoutParams layoutParams = window.getAttributes(); layoutParams.alpha = alpha; window.setAttributes(layoutParams); } }
然后定义透明度
private float mAlpha = 1f; //背景灰度 0-1 1表示全透明
最后在PopupWindow show的时候调用以下方法。
/** * 窗口显示,窗口背景透明度渐变动画 */ private void showBackgroundAnimator() { if (mAlpha >= 1f) return; ValueAnimator animator = ValueAnimator.ofFloat(1.0f, mAlpha); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float alpha = (float) animation.getAnimatedValue(); setWindowBackgroundAlpha(alpha); } }); animator.setDuration(360); animator.start(); }
通过动画来改变Window达到渐变的效果。
已有的库
这么麻烦的弹框,当然有人已经为我们封装好了
RelativePopupWindow:代码简洁,支持各种位置的弹框。还能超出屏幕(感觉用不上)。
用起来是这样的:
popup.showOnAnchor(anchor, VerticalPosition.ABOVE, HorizontalPosition.CENTER, false);
EasyPopup:一个功能比较全的库,支持背景变暗,背景不可点击(6.0以上通用)等...而且可以链式调用哦。不过有个缺点:背景变暗效果只支持 4.2 以上的版本。
用起来是这样的:
private EasyPopup mCirclePop; circlePop = new EasyPopup(this) .setContentView(R.layout.layout_circle_comment) .setAnimationStyle(R.style.CirclePopAnim) //是否允许点击PopupWindow之外的地方消失 .setFocusAndOutsideEnable(true) .createPopup(); //显示 circlePop.showAtAnchorView(view, VerticalGravity.CENTER, HorizontalGravity.LEFT, 0, 0);
这两个库的跟我上面的思路基本一样,然后通过水平、竖直参数来使用。
用的话,如果最小版本大于等于18的话,直接用- EasyPopup就可以了。(毕竟是国人写的,有中文文档~~)
自己写个工具类
介于EasyPopup只适配4.2 以上的版本,而项目要适配到4.1。只好自己写一个了~~
结合上面的提到的两个库,以及背景变暗的方案。改造一个自己的库。具体的实现代码就不贴出来了,用法也借鉴了上面的两个库。
SmartPopupWindow popupWindow= SmartPopupWindow.Builder .build(Activity.this, view) .setAlpha(0.4f) //背景灰度 默认全透明 .setOutsideTouchDismiss(false) //点击外部消失 默认true(消失) .createPopupWindow(); //创建PopupWindow popupWindow.showAtAnchorView(view, VerticalPosition.ABOVE, HorizontalPosition.CENTER);
水平方向参数HorizontalPosition:LEFT 、 RIGHT 、 ALIGN_LEFT 、 ALIGN_RIGHT、 CENTER
竖直方向参数VerticalPosition :ABOVE 、 BELOW、 ALIGN_TOP 、 ALIGN_BOTTOM、 CENTER
功能很简单,由于是通过别人的库改造的,就不上传JCenter了。
里面就三个文件,想用的话去拷下来就可以了。
参考
Android弹窗_PopupWindow详解 (挺详细的)
作者:带心情去旅行