You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android跨Activity触发消息展示的实现方案咨询(无Overlay权限)

解决方案

针对你遇到的悬浮窗权限受限问题,我们可以通过全局状态管理+Activity DecorView注入的方案实现需求,无需依赖特殊系统权限。

核心思路

  1. 用全局单例管理消息的状态(内容、展示时机、位置等)
  2. 基于Activity的DecorView动态添加消息View,替代悬浮窗
  3. 通过全局生命周期监听,统一处理不同Activity的消息展示逻辑

具体实现

1. 全局消息状态管理器

创建单例类统一管理消息的展示规则、状态和位置:

object MessageManager {
    private var currentMessage: String? = null
    private var showStartTime: Long = 0L
    private var currentPosition: Pair<Int, Int>? = null
    private var displayPeriod: Long = 0L
    private var totalPeriod: Long = 0L
    private var isMessageActive: Boolean = false
    private var nextShowTime: Long = 0L
    private val handler = Handler(Looper.getMainLooper())
    private var hideRunnable: Runnable? = null

    // 服务器触发消息时调用此方法
    fun triggerMessage(message: String, displayPeriod: Long, totalPeriod: Long) {
        this.currentMessage = message
        this.displayPeriod = displayPeriod
        this.totalPeriod = totalPeriod
        this.isMessageActive = true
        this.nextShowTime = System.currentTimeMillis()
        notifyCurrentActivity()
    }

    // 检查当前Activity是否需要展示消息
    fun checkAndShowMessage(activity: Activity) {
        if (!isMessageActive || currentMessage.isNullOrEmpty()) return

        val now = System.currentTimeMillis()
        when (activity) {
            is PlayActivity -> handlePlayActivityShow(activity, now)
            else -> handleOtherActivityShow(activity, now)
        }
    }

    private fun handlePlayActivityShow(activity: Activity, now: Long) {
        if (now >= nextShowTime) {
            // 生成随机位置(基于当前窗口尺寸)
            val windowSize = activity.windowManager.currentWindowMetrics.bounds
            val maxX = windowSize.width() - 200 // 消息宽度预留值
            val maxY = windowSize.height() - 100 // 消息高度预留值
            val randomX = Random().nextInt(maxX)
            val randomY = Random().nextInt(maxY)
            currentPosition = Pair(randomX, randomY)

            // 显示消息并设置隐藏延迟
            showMessageInActivity(activity, currentPosition!!)
            showStartTime = now
            hideRunnable = Runnable {
                hideMessageInActivity(activity)
                nextShowTime = now + totalPeriod
                // 预约下次展示
                handler.postDelayed({
                    if (isMessageActive && activity.isResumed) {
                        checkAndShowMessage(activity)
                    }
                }, totalPeriod - displayPeriod)
            }
            handler.postDelayed(hideRunnable!!, displayPeriod)
        }
    }

    private fun handleOtherActivityShow(activity: Activity, now: Long) {
        // 仅在切换后displayPeriod时长内展示消息
        if (now - showStartTime <= displayPeriod) {
            currentPosition?.let { showMessageInActivity(activity, it) }
        }
    }

    private fun showMessageInActivity(activity: Activity, position: Pair<Int, Int>) {
        // 先移除已存在的消息View
        val decorView = activity.window.decorView as ViewGroup
        val existingView = decorView.findViewById<MessageView>(R.id.message_view)
        existingView?.let { decorView.removeView(it) }

        // 添加新的消息View
        val messageView = MessageView(activity).apply {
            id = R.id.message_view
            setMessage(currentMessage!!)
            x = position.first.toFloat()
            y = position.second.toFloat()
        }
        decorView.addView(messageView)
    }

    private fun hideMessageInActivity(activity: Activity) {
        val decorView = activity.window.decorView as ViewGroup
        val messageView = decorView.findViewById<MessageView>(R.id.message_view)
        messageView?.let { decorView.removeView(it) }
    }

    fun clearMessage() {
        isMessageActive = false
        hideRunnable?.let { handler.removeCallbacks(it) }
        currentMessage = null
        currentPosition = null
    }

    private fun notifyCurrentActivity() {
        CurrentActivityHolder.currentActivity?.let { checkAndShowMessage(it) }
    }
}

// 保存当前前台Activity的工具类
object CurrentActivityHolder {
    var currentActivity: Activity? = null
}

2. 全局Activity生命周期监听

在库端的Init类中注册全局回调,统一处理所有Activity的消息展示/清理:

class Init {
    fun init(application: Application) {
        application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
            override fun onActivityStarted(activity: Activity) {}
            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
            override fun onActivityStopped(activity: Activity) {}
            override fun onActivityDestroyed(activity: Activity) {}

            override fun onActivityResumed(activity: Activity) {
                CurrentActivityHolder.currentActivity = activity
                MessageManager.checkAndShowMessage(activity)
            }

            override fun onActivityPaused(activity: Activity) {
                // 清理当前Activity的消息View,避免内存泄漏
                val decorView = activity.window.decorView as ViewGroup
                val messageView = decorView.findViewById<MessageView>(R.id.message_view)
                messageView?.let { decorView.removeView(it) }
                if (CurrentActivityHolder.currentActivity == activity) {
                    CurrentActivityHolder.currentActivity = null
                }
            }
        })
    }
}

3. 自定义消息View

创建用于展示消息的自定义View:

class MessageView(context: Context) : FrameLayout(context) {
    init {
        LayoutInflater.from(context).inflate(R.layout.layout_message, this, true)
    }

    fun setMessage(message: String) {
        findViewById<TextView>(R.id.tv_message).text = message
    }
}

对应的布局文件layout_message.xml:

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tv_message"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="8dp"
    android:background="#CC000000"
    android:textColor="#FFFFFF"
    android:textSize="14sp"/>

关键注意事项

  • 内存泄漏防护:在ActivityonPause时必须移除消息View,避免持有Activity引用
  • 屏幕适配:位置计算基于当前窗口尺寸,支持多窗口、旋转等场景
  • 线程安全:服务器触发消息时,确保在主线程调用MessageManager.triggerMessage
  • 模块初始化:在Launcher端的Application中调用Init.init(application),确保全局回调生效

内容的提问来源于stack exchange,提问作者insider_ank

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 23:11:16