package com.lzf.easyfloat import android.app.Activity import android.content.Context import android.view.View import com.lzf.easyfloat.core.FloatingWindowManager import com.lzf.easyfloat.data.FloatConfig import com.lzf.easyfloat.enums.ShowPattern import com.lzf.easyfloat.enums.SidePattern import com.lzf.easyfloat.interfaces.* import com.lzf.easyfloat.interfaces.OnPermissionResult import com.lzf.easyfloat.permission.PermissionUtils import com.lzf.easyfloat.utils.LifecycleUtils import com.lzf.easyfloat.interfaces.FloatCallbacks import com.lzf.easyfloat.utils.DisplayUtils import com.lzf.easyfloat.utils.Logger import org.greenrobot.eventbus.EventBus import java.lang.Exception /** * @author: liuzhenfeng * @github:https://github.com/princekin-f * @function: 悬浮窗使用工具类 * @date: 2019-06-27 15:22 */ class EasyFloat { companion object { /** * 通过上下文,创建浮窗的构建者信息,使浮窗拥有一些默认属性 * @param activity 上下文信息,优先使用Activity上下文,因为系统浮窗权限的自动申请,需要使用Activity信息 * @return 浮窗属性构建者 */ @JvmStatic fun with(activity: Context): Builder = if (activity is Activity) Builder(activity) else Builder(LifecycleUtils.getTopActivity() ?: activity) /** * 关闭当前浮窗 * @param tag 浮窗标签 * @param force 立即关闭,有退出动画也不执行 */ @JvmStatic @JvmOverloads fun dismiss(tag: String? = null, force: Boolean = false) = FloatingWindowManager.dismiss(tag, force) /** * 隐藏当前浮窗 * @param tag 浮窗标签 */ @JvmStatic @JvmOverloads fun hide(tag: String? = null) = FloatingWindowManager.visible(false, tag, false) /** * 设置当前浮窗可见 * @param tag 浮窗标签 */ @JvmStatic @JvmOverloads fun show(tag: String? = null) = FloatingWindowManager.visible(true, tag, true) /** * 设置当前浮窗是否可拖拽,先获取浮窗的config,后修改相应属性 * @param dragEnable 是否可拖拽 * @param tag 浮窗标签 */ @JvmStatic @JvmOverloads fun dragEnable(dragEnable: Boolean, tag: String? = null) = getConfig(tag)?.let { it.dragEnable = dragEnable } /** * 获取当前浮窗是否显示,通过浮窗的config,获取显示状态 * @param tag 浮窗标签 * @return 当前浮窗是否显示 */ @JvmStatic @JvmOverloads fun isShow(tag: String? = null) = getConfig(tag)?.isShow ?: false /** * 获取当前浮窗中,我们传入的View * @param tag 浮窗标签 */ @JvmStatic @JvmOverloads fun getFloatView(tag: String? = null): View? = getConfig(tag)?.layoutView /** * 更新浮窗坐标,未指定坐标执行吸附动画 * @param tag 浮窗标签 * @param x 更新后的X轴坐标 * @param y 更新后的Y轴坐标 */ @JvmStatic @JvmOverloads fun updateFloat(tag: String? = null, x: Int = -1, y: Int = -1) = FloatingWindowManager.getHelper(tag)?.updateFloat(x, y) // 以下几个方法为:系统浮窗过滤页面的添加、移除、清空 /** * 为当前浮窗过滤,设置需要过滤的Activity * @param activity 需要过滤的Activity * @param tag 浮窗标签 */ @JvmStatic @JvmOverloads fun filterActivity(activity: Activity, tag: String? = null) = getFilterSet(tag)?.add(activity.componentName.className) /** * 为当前浮窗,设置需要过滤的Activity类名(一个或者多个) * @param tag 浮窗标签 * @param clazz 需要过滤的Activity类名,一个或者多个 */ @JvmStatic @JvmOverloads fun filterActivities(tag: String? = null, vararg clazz: Class<*>) = getFilterSet(tag)?.addAll(clazz.map { it.name }) /** * 为当前浮窗,移除需要过滤的Activity * @param activity 需要移除过滤的Activity * @param tag 浮窗标签 */ @JvmStatic @JvmOverloads fun removeFilter(activity: Activity, tag: String? = null) = getFilterSet(tag)?.remove(activity.componentName.className) /** * 为当前浮窗,移除需要过滤的Activity类名(一个或者多个) * @param tag 浮窗标签 * @param clazz 需要移除过滤的Activity类名,一个或者多个 */ @JvmStatic @JvmOverloads fun removeFilters(tag: String? = null, vararg clazz: Class<*>) = getFilterSet(tag)?.removeAll(clazz.map { it.name }) /** * 清除当前浮窗的所有过滤信息 * @param tag 浮窗标签 */ @JvmStatic @JvmOverloads fun clearFilters(tag: String? = null) = getFilterSet(tag)?.clear() /** * 获取当前浮窗的config * @param tag 浮窗标签 */ private fun getConfig(tag: String?) = FloatingWindowManager.getHelper(tag)?.config /** * 获取当前浮窗的过滤集合 * @param tag 浮窗标签 */ private fun getFilterSet(tag: String?) = getConfig(tag)?.filterSet } /** * 浮窗的属性构建类,支持链式调用 */ class Builder(private val activity: Context) : OnPermissionResult { // 创建浮窗数据类,方便管理配置 private val config = FloatConfig() /** * 设置浮窗的吸附模式 * @param sidePattern 浮窗吸附模式 */ fun setSidePattern(sidePattern: SidePattern) = apply { config.sidePattern = sidePattern } /** * 设置浮窗的显示模式 * @param showPattern 浮窗显示模式 */ fun setShowPattern(showPattern: ShowPattern) = apply { config.showPattern = showPattern } /** * 设置浮窗的布局文件,以及布局的操作接口 * @param layoutId 布局文件的资源Id * @param invokeView 布局文件的操作接口 */ @JvmOverloads fun setLayout(layoutId: Int, invokeView: OnInvokeView? = null) = apply { config.layoutId = layoutId config.invokeView = invokeView } /** * 设置浮窗的对齐方式,以及偏移量 * @param gravity 对齐方式 * @param offsetX 目标坐标的水平偏移量 * @param offsetY 目标坐标的竖直偏移量 */ @JvmOverloads fun setGravity(gravity: Int, offsetX: Int = 0, offsetY: Int = 0) = apply { config.gravity = gravity config.offsetPair = Pair(offsetX, offsetY) } /** * 设置浮窗的起始坐标,优先级高于setGravity * @param x 起始水平坐标 * @param y 起始竖直坐标 */ fun setLocation(x: Int, y: Int) = apply { config.locationPair = Pair(x, y) } /** * 设置浮窗的拖拽边距值 * @param left 浮窗左侧边距 * @param top 浮窗顶部边距 * @param right 浮窗右侧边距 * @param bottom 浮窗底部边距 */ @JvmOverloads fun setBorder( left: Int = 0, top: Int = -DisplayUtils.getStatusBarHeight(activity), right: Int = DisplayUtils.getScreenWidth(activity), bottom: Int = DisplayUtils.getScreenHeight(activity) ) = apply { config.leftBorder = left config.topBorder = top config.rightBorder = right config.bottomBorder = bottom } /** * 设置浮窗的标签:只有一个浮窗时,可以不设置; * 有多个浮窗必须设置不容的浮窗,不然没法管理,所以禁止创建相同标签的浮窗 * @param floatTag 浮窗标签 */ fun setTag(floatTag: String?) = apply { config.floatTag = floatTag } /** * 设置浮窗是否可拖拽 * @param dragEnable 是否可拖拽 */ fun setDragEnable(dragEnable: Boolean) = apply { config.dragEnable = dragEnable } /** * 设置浮窗是否状态栏沉浸 * @param immersionStatusBar 是否状态栏沉浸 */ fun setImmersionStatusBar(immersionStatusBar: Boolean) = apply { config.immersionStatusBar = immersionStatusBar } /** * 浮窗是否包含EditText,浮窗默认不获取焦点,无法弹起软键盘,所以需要适配 * @param hasEditText 是否包含EditText */ fun hasEditText(hasEditText: Boolean) = apply { config.hasEditText = hasEditText } /** * 通过传统接口,进行浮窗的各种状态回调 * @param callbacks 浮窗的各种事件回调 */ fun registerCallbacks(callbacks: OnFloatCallbacks) = apply { config.callbacks = callbacks } /** * 针对kotlin 用户,传入带FloatCallbacks.Builder 返回值的 lambda,可按需回调 * 为了避免方法重载时 出现编译错误的情况,更改了方法名 * @param builder 事件回调的构建者 */ fun registerCallback(builder: FloatCallbacks.Builder.() -> Unit) = apply { config.floatCallbacks = FloatCallbacks().apply { registerListener(builder) } } /** * 设置浮窗的出入动画 * @param floatAnimator 浮窗的出入动画,为空时不执行动画 */ fun setAnimator(floatAnimator: OnFloatAnimator?) = apply { config.floatAnimator = floatAnimator } /** * 设置屏幕的有效显示高度(不包含虚拟导航栏的高度) * @param displayHeight 屏幕的有效高度 */ fun setDisplayHeight(displayHeight: OnDisplayHeight) = apply { config.displayHeight = displayHeight } /** * 设置浮窗宽高是否充满屏幕 * @param widthMatch 宽度是否充满屏幕 * @param heightMatch 高度是否充满屏幕 */ fun setMatchParent(widthMatch: Boolean = false, heightMatch: Boolean = false) = apply { config.widthMatch = widthMatch config.heightMatch = heightMatch } /** * 设置需要过滤的Activity类名,仅对系统浮窗有效 * @param clazz 需要过滤的Activity类名 */ fun setFilter(vararg clazz: Class<*>) = apply { clazz.forEach { config.filterSet.add(it.name) if (activity is Activity) { // 过滤掉当前Activity if (it.name == activity.componentName.className) config.filterSelf = true } } } /** * 创建浮窗,包括Activity浮窗和系统浮窗,如若系统浮窗无权限,先进行权限申请 */ fun show() = when { // 未设置浮窗布局文件,不予创建 config.layoutId == null -> callbackCreateFailed(WARN_NO_LAYOUT) // 仅当页显示,则直接创建activity浮窗 config.showPattern == ShowPattern.CURRENT_ACTIVITY -> createFloat() // 系统浮窗需要先进行权限审核,有权限则创建app浮窗 PermissionUtils.checkPermission(activity) -> createFloat() // 申请浮窗权限 else -> { WindowDIalog(activity,object : WindowDIalog.OnDialogListener{ override fun onClickImgCancle() { } override fun onClickCancle() { } override fun onClickSure(position: Int?) { requestPermission() } }).show() } } /** * 通过浮窗管理类,统一创建浮窗 */ private fun createFloat() = FloatingWindowManager.create(activity, config) /** * 通过Fragment去申请系统悬浮窗权限 */ private fun requestPermission() = if (activity is Activity) PermissionUtils.requestPermission(activity, this) else callbackCreateFailed(WARN_CONTEXT_REQUEST) /** * 申请浮窗权限的结果回调 * @param isOpen 悬浮窗权限是否打开 */ override fun permissionResult(isOpen: Boolean) = if (isOpen) { // EventBus.getDefault().post(WindowEvent(127)) // createFloat() } else callbackCreateFailed(WARN_PERMISSION) /** * 回调创建失败 * @param reason 失败原因 */ private fun callbackCreateFailed(reason: String) { config.callbacks?.createdResult(false, reason, null) config.floatCallbacks?.builder?.createdResult?.invoke(false, reason, null) Logger.w(reason) if (reason == WARN_NO_LAYOUT || reason == WARN_UNINITIALIZED || reason == WARN_CONTEXT_ACTIVITY) { // 针对无布局、未按需初始化、Activity浮窗上下文错误,直接抛异常 throw Exception(reason) } } } }