限时福利领取


在开发即时通讯类应用时,通话提示功能是核心体验之一。微信的语音/视频通话提示有三大特征:持续响铃提醒、悬浮窗展示通话界面、应用最小化后仍保持可见。传统的Notification或简单WindowManager实现难以满足这些需求,今天我们就来深入探讨如何高仿微信的通话提示效果。

通话提示悬浮窗示意图

一、技术方案对比

先来看几种常见实现方式的优劣对比:

| 方案 | CPU占用 | 内存占用 | 保活能力 | 兼容性 | |---------------------|---------|----------|----------|--------| | 系统Notification | 低 | 低 | 差 | 好 | | 悬浮窗(Permission) | 中 | 中 | 好 | 一般 | | 前台服务+Activity | 高 | 高 | 优秀 | 好 |

通过对比可以看出,前台服务+自定义View的悬浮窗方案在保活能力和用户体验上最接近微信的效果,虽然资源消耗略高,但通过优化可以控制在合理范围。

二、核心实现步骤

  1. 基础架构搭建

我们需要组合使用ForegroundService和WindowManager来实现悬浮窗效果。ForegroundService保证进程优先级,WindowManager负责界面展示。

  1. 权限处理(关键!)

在AndroidManifest.xml中声明权限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

动态请求权限的Kotlin代码:

private fun checkOverlayPermission() {
    if (!Settings.canDrawOverlays(this)) {
        Intent(
            Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
            Uri.parse("package:$packageName")
        ).apply {
            startActivityForResult(this, REQUEST_OVERLAY_PERMISSION)
        }
    }
}
  1. 悬浮窗实现关键代码

WindowManager的典型配置:

val params = WindowManager.LayoutParams(
    WindowManager.LayoutParams.WRAP_CONTENT,
    WindowManager.LayoutParams.WRAP_CONTENT,
    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 or Gravity.START
    x = 100
    y = 100
}
  1. 触摸事件处理(仿微信滑动接听)
override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            initialX = params.x
            initialY = params.y
            initialTouchX = event.rawX
            initialTouchY = event.rawY
            return true
        }
        MotionEvent.ACTION_MOVE -> {
            params.x = initialX + (event.rawX - initialTouchX).toInt()
            params.y = initialY + (event.rawY - initialTouchY).toInt()
            windowManager.updateViewLayout(this, params)
            return true
        }
        MotionEvent.ACTION_UP -> {
            // 处理接听/挂断逻辑
            return true
        }
    }
    return false
}

权限请求示意图

三、避坑指南

  1. 内存泄漏预防

务必在Service的onDestroy中移除View:

override fun onDestroy() {
    super.onDestroy()
    windowManager.removeViewImmediate(callView)
}
  1. 异形屏适配

获取安全区域避免被刘海遮挡:

val windowInsets = ViewCompat.getRootWindowInsets(view)
val safeInsetTop = windowInsets?.stableInsetTop ?: 0
  1. ANR避免

铃声播放要使用异步方式:

MediaPlayer.create(context, R.raw.ringtone).apply {
    setOnPreparedListener { it.start() }
    prepareAsync()
}

四、性能优化建议

  1. 使用JobScheduler在应用进入后台时降低更新频率
  2. 对悬浮窗View进行离屏渲染优化
  3. 铃声资源使用适当压缩的音频格式

五、思考题

如何实现挂断后自动恢复原应用界面?可以考虑以下几种方案:

  1. 在通话服务中记录当前顶部Activity
  2. 使用ActivityManager获取任务栈信息
  3. 通过包名启动原应用的主Activity

希望这篇文章能帮助你实现媲美微信的通话提示体验。如果遇到具体实现问题,欢迎在评论区交流讨论!

Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐