*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
一 效果图
完整代码地址已上传Github
:PopWindow
二 封装通用PopupWindow
PopupWindow
这个类用来实现一个弹出框,可以使用任意布局的View
作为其内容,这个弹出框是悬浮在当前Activity
之上的,一般PopupWindow
的使用:
//准备PopupWindow的布局View
View popupView = LayoutInflater.from(this).inflate(R.layout.popup, null);
//初始化一个PopupWindow,width和height都是WRAP_CONTENT
PopupWindow popupWindow = new PopupWindow(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
//设置PopupWindow的视图内容
popupWindow.setContentView(popupView);
//点击空白区域PopupWindow消失,这里必须先设置setBackgroundDrawable,否则点击无反应
popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));
popupWindow.setOutsideTouchable(true);
//设置PopupWindow动画
popupWindow.setAnimationStyle(R.style.AnimDown);
//设置是否允许PopupWindow的范围超过屏幕范围
popupWindow.setClippingEnabled(true);
//设置PopupWindow消失监听
popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
}
});
//PopupWindow在targetView下方弹出
popupWindow.showAsDropDown(targetView);
上面就是PopupWindow
通常需要设置的各个方法,每次使用时都需要一堆设置,稍微有点繁琐,有些是可以复用的,所以可以封装成一个通用的PopWindow
:
PopWindow
继承自PopupWindow
,拥有PopupWindow
的各个属性方法,使用类似建造者模式,和AlertDialog
的使用方式差不多,PopWindow
使用举例:
val pop = PopWindow.Builder(this)
//设置PopupWindow布局
.setView(R.layout.popup_down)
//设置宽高
.setWidthAndHeight(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
//设置动画
.setAnimStyle(R.style.AnimDown)
//设置背景颜色,取值范围0.0f-1.0f 值越小越暗 1.0f为透明
.setBackGroundLevel(0.5f)
//设置PopupWindow里的子View及点击事件
.setChildrenView(object : PopWindow.ViewInterface {
override fun getChildView(view: View, layoutResId: Int, pop: PopupWindow) {
val tv_child = view.findViewById(R.id.tv_child);
tv_child.setText("我是子View");
}
})
//设置外部是否可点击 默认是true
.setOutsideTouchable(true)
//开始构建
.create()
//弹出PopupWindow
pop.showAsDropDown(view);
除了自行设置弹窗的方向外,还内置了上下左右各个方向的弹出,符合需求的可以直接使用,对应方法如下(完整代码见github
):
/**
* 在当前View的上方展示
* @param target 目标View
* @param gravity
* @param wExtra x轴微调使用 >0时向右移,<0时向左移
* @param hExtra y轴微调使用 >0时向下移, <0时向上移
*/
fun showOnTargetTop(
target: View,
gravity: Int = CENTER_TOP,
wExtra: Int = 0,
hExtra: Int = 0,
) {
var xOff = 0
when (gravity) {
CENTER_TOP -> {
//正上方
xOff = -(width - target.measuredWidth) / 2
}
LEFT_TOP -> {
//左上方
xOff = -(width - target.measuredWidth / 2)
}
RIGHT_TOP -> {
//右上方
xOff = target.measuredWidth / 2
}
}
showAsDropDown(target, xOff + wExtra, -(height + target.measuredHeight) + hExtra)
}
/**
* 在当前View的下方展示
* @param target 目标View
* @param gravity
* @param wExtra x轴微调使用 >0时向右移,<0时向左移
* @param hExtra y轴微调使用 >0时向下移, <0时向上移
*/
fun showOnTargetBottom(
target: View,
gravity: Int = CENTER_BOTTOM,
wExtra: Int = 0,
hExtra: Int = 0,
) {
var xOff = 0
when (gravity) {
CENTER_BOTTOM -> {
//正下方
xOff = -(width - target.measuredWidth) / 2
}
LEFT_BOTTOM -> {
//左下方
xOff = -(width - target.measuredWidth / 2)
}
RIGHT_BOTTOM -> {
//右下方
xOff = target.measuredWidth / 2
}
}
showAsDropDown(target, xOff + wExtra, hExtra)
}
/**
* 在当前View的右侧展示
* @param target 目标View
* @param gravity
* @param wExtra x轴微调使用 >0时向右移,<0时向左移
* @param hExtra y轴微调使用 >0时向下移, <0时向上移
*/
fun showOnTargetRight(
target: View,
gravity: Int = CENTER_RIGHT,
wExtra: Int = 0,
hExtra: Int = 0,
) {
var hOff = 0
when (gravity) {
CENTER_RIGHT -> {
//右侧
hOff = -(height + target.height) / 2
}
}
showAsDropDown(target, target.measuredWidth + wExtra, hOff + hExtra)
}
/**
* 在当前View的左侧展示
* @param target 目标View
* @param gravity
* @param wExtra x轴微调使用 >0时向右移,<0时向左移
* @param hExtra y轴微调使用 >0时向下移, <0时向上移
*/
fun showOnTargetLeft(
target: View,
gravity: Int = CENTER_LEFT,
wExtra: Int = 0,
hExtra: Int = 0,
) {
var hOff = 0
when (gravity) {
CENTER_LEFT -> {
//左侧
hOff = -(height + target.height) / 2
}
}
showAsDropDown(target, -width + wExtra, hOff + hExtra)
}
const val CENTER_TOP = 0 //正上方
const val LEFT_TOP = 1 //左上方
const val RIGHT_TOP = 2 //右上方
const val CENTER_BOTTOM = 3 //正下方
const val LEFT_BOTTOM = 4 //左下方
const val RIGHT_BOTTOM = 5 //右下方
const val CENTER_RIGHT = 6 //右侧
const val CENTER_LEFT = 7 //左侧
2.1 设置背景
这里使用的是WindowManager.LayoutParams.alpha
属性,看下官网对其解释:An alpha value to apply to this entire window. An alpha of 1.0 means fully opaque and 0.0 means fully transparent。 alpha
值适用于整个Window
,α为1.0时表示完全不透明而0.0表示完全透明,默认是1.0
,当PopupWindow
弹出时通过设置alpha
在(0.0,1.0)
之间设置灰色背景,当PopupWindow
消失时恢复默认值。
/**
* 设置背景灰色程度
*
* @param level 0.0f-1.0f
*/
fun setBackGroundLevel(level: Float) {
mWindow = (context as Activity).window
val params = mWindow?.attributes
params?.alpha = level
mWindow?.attributes = params
}
2.2 计算宽高
//设置测量模式为UNSPECIFIED可以确保测量不受父View的影响
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
view?.measure(widthMeasureSpec, heightMeasureSpec)
//得到测量宽度
val mWidth=view.getMeasuredWidth();
//得到测量高度
val mHeight=view.getMeasuredHeight();
注:在测量宽高时遇到一种情况,如图所示:
如果设置TextView
的 android:layout_width="wrap_content"
,那么测量不出TextView
准确的height
,当设置width
为某个确定值时,也能得到准确的height
了。
2.3 设置动画
如设置向右动画:
.setAnimationStyle(R.style.AnimHorizontal);
在style.xml文件中设置:
<style name="AnimHorizontal" parent="@android:style/Animation">
<item name="android:windowEnterAnimation">@anim/push_scale_left_in</item>
<item name="android:windowExitAnimation">@anim/push_scale_left_out</item>
</style>
android:windowEnterAnimation
、android:windowExitAnimation
分别为Popupwindow
弹出和消失动画
进入动画为anim
目录下的 push_scale_left_in.xml:
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:fromXScale="0.0"
android:fromYScale="1.0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toXScale="1.0"
android:toYScale="1.0" />
消失动画为 push_scale_left_out.xml:
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toXScale="0.0"
android:toYScale="1.0" />
2.4 弹出位置原理
因为PopWindow
继承自PopupWindow
,所以可以直接使用PopupWindow
中的弹出方法,常用的下面三种:
public void showAsDropDown(View anchor)
public void showAsDropDown(View anchor, int xoff, int yoff)
public void showAtLocation(View parent, int gravity, int x, int y)
其中,showAsDropDown
是显示在参照物anchor
的周围,xoff、yoff分别是X轴、Y轴的偏移量
,如果不设置xoff、yoff
,默认是显示在anchor
的下方;showAtLocation
是设置在父控件的位置,如设置Gravity.BOTTOM
表示在父控件底部弹出,xoff、yoff
也是X轴、Y轴
的偏移量。
如上面向右弹出例子,分别使用showAsDropDown
和showAtLocation
来实现:
2.4.1 showAsDropDown
popupWindow.showAsDropDown(view, view.getWidth(), -view.getHeight());
showAsDropDown
默认展示在button
的下面,通过改变X轴
和Y轴
的偏移量(X轴
向右偏移button
的宽度,Y轴
向上偏移button
的高度),实现在Button
右边弹出。
2.4.2 showAtLocation
int[] positions = new int[2];
view.getLocationOnScreen(positions);
popupWindow.showAtLocation(findViewById(android.R.id.content),
Gravity.START| Gravity.TOP , positions[0] + view.getWidth(), positions[1]);
使用了View
的getLocationOnScreen
方法来获得View在屏幕中的坐标位置,传入的参数必须是一个有2个整数的数组,分别代表View
的X、Y坐标
,即是View
的左上角的坐标,这里的View
是Button
,知道了Button
左上角的坐标,就可以得到要展示的PopupWindow
的左上角的坐标为(positions[0] + view.getWidth(), positions[1])
,从而实现在Button
右边弹出。