Flutter基于Kotlin BroadcastReceiver实现Android悬浮窗方案
Android 端实现类CallKit来电悬浮窗的可行方案
这个场景属于原生端主动触发UI展示,和Flutter调原生方法的正向调用逻辑不一样,不要硬套官方文档里Flutter调Kotlin的实现路径,按以下步骤实现即可:
1. 前置配置
先在android/app/src/main/AndroidManifest.xml中添加必要权限与组件注册:
- 基础权限声明
<!-- 悬浮窗权限 --> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 来电状态读取权限 --> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <!-- 前台服务权限,Android 9+ 必须 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
- 组件注册,注意Android 12+必须显式声明
exported属性
<!-- 来电事件广播接收器 --> <receiver android:name=".CallEventReceiver" android:exported="true"> <intent-filter> <action android:name="android.intent.action.PHONE_STATE" /> </intent-filter> </receiver> <!-- 悬浮窗承载服务 --> <service android:name=".OverlayWindowService" android:exported="false" android:foregroundServiceType="phoneCall" />
2. 广播接收器事件处理
不要直接在BroadcastReceiver里弹悬浮窗,Android后台启动限制会直接拦截,Receiver只做事件校验和服务转发:
class CallEventReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val callState = intent.getStringExtra(TelephonyManager.EXTRA_STATE) // 仅处理响铃状态 if (callState == TelephonyManager.EXTRA_STATE_RINGING) { val incomingNum = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) // 先校验悬浮窗权限,未授权则跳权限申请页 if (!Settings.canDrawOverlays(context)) { val permIntent = Intent( Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:${context.packageName}") ).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } context.startActivity(permIntent) return } // 启动悬浮窗服务,传来电信息 val serviceIntent = Intent(context, OverlayWindowService::class.java).apply { putExtra("incoming_number", incomingNum) action = "ACTION_SHOW_CALL_UI" } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(serviceIntent) } else { context.startService(serviceIntent) } } } }
3. 悬浮窗服务实现
在OverlayWindowService中通过WindowManager加载来电UI,优先用原生XML布局实现类CallKit效果,加载速度比Flutter组件更快,不会出现来电时UI加载延迟的问题:
class OverlayWindowService : Service() { private lateinit var windowManager: WindowManager private var callView: View? = null override fun onBind(intent: Intent?): IBinder? = null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // 必须在服务启动10秒内挂载前台通知,否则会被系统杀掉 val notification = NotificationCompat.Builder(this, "call_channel") .setContentTitle("来电中") .setSmallIcon(R.mipmap.ic_launcher) .setPriority(NotificationCompat.PRIORITY_HIGH) .build() startForeground(1001, notification) if (intent?.action == "ACTION_SHOW_CALL_UI") { val incomingNum = intent.getStringExtra("incoming_number") showCallOverlay(incomingNum) } return START_STICKY } private fun showCallOverlay(number: String?) { windowManager = getSystemService(WINDOW_SERVICE) as WindowManager // 悬浮窗参数配置 val layoutParams = WindowManager.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT, // 适配不同Android版本的悬浮窗类型 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT ).apply { gravity = Gravity.TOP } // 加载提前写好的类CallKit布局 callView = LayoutInflater.from(this).inflate(R.layout.layout_incoming_call, null) // 给布局里的接听、挂断按钮绑定点击事件 callView?.findViewById<Button>(R.id.btn_answer)?.setOnClickListener { // 1. 移除悬浮窗 2. 通过MethodChannel通知Flutter层接听逻辑 removeOverlay() // 拿全局缓存的FlutterEngine实例发消息即可 } callView?.findViewById<Button>(R.id.btn_reject)?.setOnClickListener { // 1. 移除悬浮窗 2. 通知Flutter层挂断逻辑 removeOverlay() } windowManager.addView(callView, layoutParams) } private fun removeOverlay() { callView?.let { windowManager.removeView(it) callView = null stopSelf() } } override fun onDestroy() { super.onDestroy() removeOverlay() } }
4. 与Flutter层通信注意事项
- App启动时,在
MainActivity的configureFlutterEngine方法中,将初始化好的FlutterEngine实例存到原生全局单例中,不要每次事件触发都新建Engine和MethodChannel,否则会有极大的性能损耗 - 悬浮窗按钮触发事件时,直接从全局单例取Engine对应的MethodChannel发送消息即可,如果此时Flutter进程未存活,先启动主Activity等待Engine初始化完成后再发事件,避免事件丢失
- 如果一定要用Flutter组件写悬浮窗UI,可以将
FlutterView作为子View加到WindowManager的布局中,但不推荐,冷启动时FlutterView初始化耗时较长,会导致来电UI弹出延迟
常见坑点
- Android 10及以上版本严格限制后台启动Activity,不要试图直接拉起Flutter页面作为来电界面,会被系统拦截,悬浮窗/全屏通知是仅有的两个合规方案
- 国产ROM普遍需要额外引导用户开启「后台弹出界面」「自启动」权限,否则锁屏状态下悬浮窗可能无法正常展示
- 悬浮窗权限是特殊权限,不能和普通权限一样动态申请,必须跳系统设置页让用户手动开启
- 服务启动后必须在10秒内调用
startForeground挂载通知,否则会被系统直接判定为无响应并杀死
内容的提问来源于stack exchange,提问作者deok
相关产品推荐
相关产品推荐

