Android PopupWindow工具类 (已解决7.1以上showAsDropDown显示MATCH的PopWindow时覆盖控件的问题)

前言

PopWindow工具类

import android.app.Activity
import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.view.Display
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.widget.PopupWindow

/***
 * PopWindowUtil
 * Created by zxjie on 2021/5/17.
 * address:bailingkeji
 * describe:使用时请先调用init进行初始化,之后调用showPopWindow进行显示,该工具类采用单例设计模式
 * link:https://blog.csdn.net/weixin_46603990/article/details/116987681
 * https://juejin.cn/post/6963513638783205406/
 * example: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 *          |   -实例化对象-
 *          |   private val popWindowUtil = PopWindowUtil
 *          |
 *          |   -初始化PopWindow-
 *          |   --设置固定大小
 *          |   popWindowUtil.init(this,R.layout.pop_alert_two,295F,135F,null)
 *          |   --适应屏幕最大化
 *          |   popWindowUtil.init(this,R.layout.pop_alert_two,PopWindowUtil.MATCH,PopWindowUtil.MATCH,null)
 *          |   --基于屏幕设置宽高,,Restraint中的参数只有在前者为CHANGE时才会生效,公式为 屏幕宽高-Restraint.width/height
 *          |   popWindowUtil.init(this,R.layout.pop_alert_two,PopWindowUtil.CHANGE,PopWindowUtil.CHANGE,Restraint(20,60))
 *          |   --组合使用,宽置满,高基于屏幕设置
 *          |   popWindowUtil.init(this,R.layout.pop_alert_two,PopWindowUtil.MATCH,PopWindowUtil.CHANGE,Restraint(0,60))
 *          |
 *          |   -获取所用到的控件
 *          |   val btn_ok = popWindowUtil.getViewById(R.id.btn_ok) as TextView
 *          |
 *          |   -设置点击窗口外边,窗口消失,默认为false
 *          |   popWindowUtil.outSideTouchable(true)
 *          |
 *          |   -获取焦点,默认为false
 *          |   popWindowUtil.itemClickable(true)
 *          |
 *          |   -使用默认的阴影效果,默认为false
 *          |   popWindowUtil.shadowShowAble(true)
 *          |
 *          |   -设置PopWindow消失监听
 *          |   popWindowUtil.dismissListener = {
 *          |         -自定义背景
 *          |      }
 *          |
 *          |   -弹出PopWindow 注意:弹出的PopWindow永远不会跃出屏幕
 *          |   --基于整个屏幕居中弹出,向下偏移10dp
 *          |   popWindowUtil.showPopWindow(toolbar,true, Excursion(Gravity.CENTER,0,10),null)
 *          |   --基于整个屏幕在左上弹出,并向左偏移10dp 注意:这时候PopWindow已经显示在屏幕最左侧了,所以这个-10是无效的
 *          |   popWindowUtil.showPopWindow(toolbar,true, Excursion(Gravity.LEFT or Gravity.TOP,-10,0),null)
 *          |   --基于整个屏幕从下方滑动弹出,并在tabLayout上方
 *          |   popWindowUtil.showPopWindow(tab_layout, true, Excursion(Gravity.BOTTOM, 0, DensityUtil.px2dip(this, (tab_layout.layoutParams.height+nbHeight-4) * 1F)),
 *          |   --注意偏移的方向并不是正右下,负左上,它是根据位置约束来决定的,位置约束为BOTTOM,那么就是正上,负下,如果为TOP,就是正下,负上,x轴同理
 *          |   --不建议使用 基于toolbar控件的左下弹出 注意:为false时位置约束(Gravity.CENTER)将会失效
 *          |   popWindowUtil.showPopWindow(toolbar,false, Excursion(Gravity.CENTER,0,0),null)
 *          |
 *          |   -建议设置PopWindow弹出时按返回键关闭PopWindow,而不是关闭Activity,注意:onBackPressed()方法只有在Activity中才能重写
 *          |   override fun onBackPressed() {
 *          |        if (!popWindowUtil.disMiss()) finish()
 *          |        }
 *          |
 *          |   -关闭PopWindow
 *          |   popWindowUtil.disMiss()
 *          |
 *          |   -销毁PopWindow所使用的的对象,在onDestroy调用
 *          |   popWindowUtil.destroy()
 *          |
 *          |   -当工具类提供的方法不满足你的需求时可以调用以下方法获取PopWindow对象
 *          |   popWindowUtil.getPopWindow()
 *          |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */
object PopWindowUtil {
     
    private var winHeight: Int = 0
    private var winWidth: Int = 0
    private var view: View? = null
    private var popupWindow: PopupWindow? = null
    private var context: Context? = null
    private var canDown: Boolean = false
    private var canClick: Boolean = false
    private var canShow: Boolean = false
    private var ERROR: Boolean = false
    const val MATCH = -100F
    const val CHANGE = -1000F


    /***
     * @param context 上下文
     * @param layoutId 资源文件
     * @param width PopWindow所需宽/dp 为MATCH时全屏 为CHANGE时可设置距屏幕的距离
     * @param height PopWindow所需高/dp 为MATCH时全屏 为CHANGE时可设置距屏幕的距离
     * @param restraint 该参数可为空,宽高非CHANGE情况下无效,当width为CHANGE时Restraint.width生效,即PopWindow距屏幕左右的距离/dp,
     * 当height为CHANGE时Restraint.height生效,即PopWindow距屏幕上下的距离/dp
     */
    fun init(context: Context, layoutId: Int, width: Float, height: Float, restraint: Restraint?) {
     
        this.context = context
        if (isExist()) {
     
            popupWindow!!.dismiss()
            popupWindow = null
        }
        val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val display: Display = wm.defaultDisplay
        winHeight = display.height
        winWidth = display.width
        view = View.inflate(context, layoutId, null)
        val useWidth = when (width) {
     
            MATCH -> winWidth
            CHANGE -> (winWidth - dip2px(restraint!!.width))
            else -> dip2px(width)
        }
        ERROR = height == MATCH
        val useHeight = when {
     
            height == MATCH -> winHeight
            width == CHANGE -> (winHeight - dip2px(restraint!!.height))
            else -> dip2px(height)
        }
        popupWindow = PopupWindow(view, useWidth, useHeight)
    }

    /**
     * @param id 要获取的id
     * @return 返回一个view对象,需要强转为所需控件类型
     */
    fun getViewById(id: Int): View? =
        if (isExist()) view!!.findViewById(id)
        else throw NullPointerException("查找控件失败,请初始化")

    /**
     * @param location 一个View对象,用来约束PopWindow的位置
     * @param global 是否基于屏幕弹出 true基于屏幕弹出, false基于location控件的左下方弹出, 不建议使用false
     * @param excursion 参数一是位置约束,global为false时位置约束将会失效,参数二传入x轴的偏移/dp,,参数三传入y轴的偏移/dp,注意偏移的方向并不是正右下,负左上,
     * 它是根据位置约束来决定的,位置约束为BOTTOM,那么就是正上,负下,如果为TOP,就是正下,负上,x轴同理
     * @param anim 动画效果,选择性传入,不需要时传入空
     */
    fun showPopWindow(location: View, global: Boolean, excursion: Excursion, anim: Int?) {
     
        val disPose = disPose(location)
        val x = dip2px(excursion.x.toFloat())
        val y = dip2px(excursion.y.toFloat())
        if (isExist()) {
     
            if (anim != null) popupWindow?.animationStyle = anim
            popupWindow?.isFocusable = canClick
            if (canDown) popupWindow?.setBackgroundDrawable(BitmapDrawable())
            popupWindow?.isOutsideTouchable = canDown
            if (canShow) setBackgroundAlpha(0.5F)
            popupWindow!!.setOnDismissListener {
     
                if (canShow) setBackgroundAlpha(1F)
                dismissListener?.invoke()
            }
            if (global) {
     
                if (ERROR) popupWindow?.height = winHeight - disPose
                popupWindow?.showAtLocation(location, excursion.gravity, x, y)
            } else {
     
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ERROR) {
     
                    val height = location.layoutParams.height
                    popupWindow?.height = winHeight - (disPose + height)
                    popupWindow?.showAtLocation(location, Gravity.NO_GRAVITY, 0, disPose + height)
                } else popupWindow?.showAsDropDown(location, x, y)
            }
        } else throw NullPointerException("显示PopWindow失败,请初始化")

    }

    /**
     * 处理7.1之上Match覆盖控件的问题
     */
    fun disPose(location: View): Int {
     
        val a = IntArray(2)
        location.getLocationOnScreen(a)
        return a[1]
    }

    /**
     * 关闭PopWindow弹窗
     * @return 成功关闭为true,否则为false
     */
    fun disMiss(): Boolean =
        if (isExist() && isShowing()) {
     
            popupWindow!!.dismiss()
            true
        } else false
    
    /**
     * 获取当前PopWindow对象
     * @return isExist()为true时返回一个PopWindow对象 为false时抛出异常
     */
    fun getPopWindow() =
        if (isExist()) popupWindow
        else throw NullPointerException("获取PopWindow对象失败,PopWindow示例对象为空")

    /**
     * 设置背景
     */
    fun setBackgroundAlpha(bgAlpha: Float) {
     
        val activity: Activity = context as Activity
        val lp = activity.window.attributes
        lp.alpha = bgAlpha
        if (bgAlpha == 1f) activity.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
        else activity.window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
        activity.window.attributes = lp
    }

    /**
     *设置是否使用自带的阴影效果
     * @param canShow false 不使用, true 使用
     */
    fun shadowShowAble(canShow: Boolean) {
     
        this.canShow = canShow
    }

    /**
     * 设置是否能点击窗口外边窗口消失
     * @param canDown false 不能点击消失, true 可以点击消失
     */
    fun outSideTouchable(canDown: Boolean) {
     
        this.canDown = canDown
    }

    /**
     * 设置是否获得焦点
     * @param canClick false 不获取,无法点击, true 获取,可以点击
     */
    fun itemClickable(canClick: Boolean) {
     
        this.canClick = canClick
    }
    
    /**
     * PopWindow是否为显示状态
     * @return true 显示, false 未显示
     */
    fun isShowing() = popupWindow!!.isShowing

    /**
     * PopWindow是否实例化
     * @return true 已实例化, false 为空
     */
    fun isExist() = popupWindow != null

    /**
     * 将所用到的对象置空,建议调用
     */
    fun destroy() {
     
        dismissListener = null
        popupWindow?.dismiss()
        popupWindow = null
        context = null
        view = null
    }

    /**
     * 单位换算 dp转px
     * @return 转为px后的数值
     */
    fun dip2px(dpValue: Float): Int =
        (dpValue * this.context!!.resources.displayMetrics.density + 0.5f).toInt()

    /**
     * 通过方法变量实现接口回调
     */
    var dismissListener: (() -> Unit)? = null
}

/**
 * @param gravity 位置约束 参数为:Gravity.TOP,Gravity.BOTTOM,Gravity.LEFT,Gravity.RIGHT,Gravity.CENTER
 * @param x
 * @param y
 */
data class Excursion(val gravity: Int, val x: Int, val y: Int)

/**
 * @param width PopWindow距屏幕左右的距离
 * @param height PopWindow距屏幕上下的距离
 */
data class Restraint(val width: Float, val height: Float)

你可能感兴趣的:(android,基础,android,app,kotlin)