Android 封装一个通用的PopupWindow

1,705 阅读4分钟

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

一 效果图

GIF.gif

完整代码地址已上传GithubPopWindow

二 封装通用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 transparentalpha 值适用于整个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();

注:在测量宽高时遇到一种情况,如图所示: uncertain.png

如果设置TextViewandroid: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:windowEnterAnimationandroid: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轴的偏移量。

如上面向右弹出例子,分别使用showAsDropDownshowAtLocation来实现: right.png

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]);

使用了ViewgetLocationOnScreen方法来获得View在屏幕中的坐标位置,传入的参数必须是一个有2个整数的数组,分别代表ViewX、Y坐标,即是View的左上角的坐标,这里的ViewButton,知道了Button左上角的坐标,就可以得到要展示的PopupWindow的左上角的坐标为(positions[0] + view.getWidth(), positions[1]),从而实现在Button右边弹出。