优雅解决Android app后台悬浮窗权限问题

背景

2025年10月起,小米将对悬浮窗权限(SYSTEM_ALERT_WINDOW)的使用场景进行调整,调整方案为:

当应用从前台切换到后台时,将仅支持以下业务功能需求下继续使用悬浮窗

1.视频聊天、直播、播放视频等小窗接续播放功能

2.音乐播放软件悬浮显示歌词功能

3.灾害预警、反诈预警等预警类功能

现状:

app在后台时,悬浮窗展示司机状态、订单、抢单等相关信息,使用场景调整之后悬浮窗将无法使用

基于此,考虑使用安卓其他组件来替代,实现悬浮窗效果

安卓多任务形态直观区别

详细功能特性对比

对比三种多任务处理方式,使用画中画可以解决我们app在后台无法使用悬浮窗权限的问题

实践

画中画实现效果

画中画实现步骤

声明PiP权限

在AndroidManifest.xml中声明PiP支持:

ini 复制代码
<activity 
    android:name=".PipActivity"
    android:supportsPictureInPicture="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />
 

关键点:

  • supportsPictureInPicture="true" 启用PiP。
  • configChanges 防止Activity在PiP模式切换时重建。

进入PiP模式

在需要触发PiP的代码逻辑中,或按home键进入画中画

scss 复制代码
// Kotlin 示例
fun enterPipMode() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val params = PictureInPictureParams.Builder()
            .setAspectRatio(Rational(16, 9)) // 画面宽高比(如16:9)
            .setActions(getPipActions()) // 可选:添加操作按钮(如出车/收车)
            .build()
        activity.enterPictureInPictureMode(params)
    }
}

参数说明:

  • setAspectRatio():设置悬浮窗比例(需避免遮挡关键UI)。
  • setActions():添加交互按钮(如"出车/收车")。

处理PiP模式下的UI

在Activity中重写onPictureInPictureModeChanged,调整UI布局:

kotlin 复制代码
override fun onPictureInPictureModeChanged(
    isInPiP: Boolean,
    newConfig: Configuration
) {
    if (isInPiP) {
        // 隐藏无关控件,仅保留核心信息(比如:订单信息)
        binding.btn.visibility = View.GONE
        binding.floatPip.scaleToFitPip()
    } else {
        // 退出PiP时恢复完整UI
        binding.btn.visibility = View.VISIBLE
    }
}
  1. pip更新数据

数据同步:通过ViewModel或LocalBroadcast更新PiP窗口中的订单信息

按钮操作控制

通过RemoteAction添加PiP窗口按钮

kotlin 复制代码
private fun getPipActions(): List<RemoteAction> {
    val intent = Intent(this, PipActionReceiver::class.java).apply {
        action = "ACTION_DUTY_UPDATE"
    }
    val icon = Icon.createWithResource(this, R.drawable.ic_duty)
    return listOf(
        RemoteAction(icon, "出车", "出车按钮", PendingIntent.getBroadcast(
            this, 0, intent, PendingIntent.FLAG_IMMUTABLE
        ))
    )
}

处理按钮点击事件

kotlin 复制代码
class PipActionReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        when (intent.action) {
            ACTION_DUTY_UPDATE -> dutyUpdate()
        }
    }
}
  1. 兼容性处理

xml 复制代码
*   **检查pip支持**

<!---->

    fun isPipSupported(): Boolean {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
        } else {
            false
        }
    }

*   **降级**

<!---->

    fun checkPipAvailability() {
        if (!isPipSupported()) {
            // 降级方案:使用普通后台服务或通知
            showNotificationInstead()
        }
    }

注意:

  1. pip仅能显示当前 Activity 的 UI 内容,并且系统会自动裁剪和缩放当前 Activity 的可见区域。

  2. RemoteAction只能设置简单的按钮交互,不能显示复杂UI

  3. 如果 PiP 窗口和主应用交互复杂,需注意 taskAffinity 和 launchMode 的影响:

    1. taskAffinity:避免 PiP Activity 被错误地放入其他 Task。
    2. singleInstance:可能导致返回栈异常,慎用。

常见问题解决

问题 1:PiP 窗口未显示指定区域

原因:

  • 目标 View 的坐标未正确计算(如未考虑状态栏高度)。
  • 目标 View 在 PiP 触发时未完成布局(onCreate 中调用过早)。

修复:

kotlin 复制代码
// 确保在 View 布局完成后调用(如 onWindowFocusChanged)
override fun onWindowFocusChanged(hasFocus: Boolean) {
    super.onWindowFocusChanged(hasFocus)
    if (hasFocus && isInPictureInPictureMode) {
        updatePipRegion()
    }
}

问题 2:PiP 窗口显示空白

原因:

  • 指定的 Rect 超出 Activity 边界。
  • 目标 View 被其他控件覆盖(如 Fragment)。

修复:

scss 复制代码
// 检查 Rect 是否有效
if (visibleRect.width() > 0 && visibleRect.height() > 0) {
    setSourceRectHint(visibleRect)
}

问题 3: setSourceRectHint 无法满足需求

替代方案:自定义pip布局

  • 在 Activity 中预先设置一个仅包含目标内容的隐藏布局。
  • 进入 PiP 时显示该布局,退出时恢复原布局。
xml 复制代码
<!-- activity_pip.xml -->
<FrameLayout>
    <LinearLayout android:id="@+id/main_content"> ... </LinearLayout>
    <LinearLayout 
        android:id="@+id/pip_content"
        android:visibility="gone"> ... </LinearLayout>
</FrameLayout>
kotlin 复制代码
override fun onPictureInPictureModeChanged(
    isInPiP: Boolean,
    newConfig: Configuration
) {
    if (isInPiP) {
        binding.pipContent.visibility = View.VISIBLE
        binding.mainContent.visibility = View.GONE
    } else {
        binding.mainContent.visibility = View.VISIBLE
        binding.pipContent.visibility = View.GONE
    }
}

问题 4: 交互按钮不显示

原因:RemoteAction创建或权限问题

修复:检查PendingIntent的flags和权限声明

可应用场景拓展

  1. 导航:在接单后,司机退出App仍能悬浮显示路线,用户退出后,可悬浮路线查看司机到达位置。

  2. 订单提醒:后台时新订单以小窗形式弹出,避免错过订单。

  3. 通话兼容:接听电话时保持导航可见。

相关推荐
_祝你今天愉快5 小时前
UI核心 - UI绘制流程及原理
android
mmsx5 小时前
Android 一个设备多个 App 抢:跨进程共享服务的架构设计
android·kotlin·app
mmsx5 小时前
Android GIS系列 栅格怎么存、格网怎么归算:两个容易混淆的 GIS 链路
android·性能优化·app
陈柒吖5 小时前
安卓代码加固(1):加密DEX
android·前端
mmsx5 小时前
Android GIS系列 串口、蓝牙、USB、网络:一套统一抽象怎么扛住四种通道
android·app·产品
二流小码农5 小时前
鸿蒙开发:ArrayList,可不会让UI更新哦
android·ios·harmonyos
hai_android6 小时前
深入理解 Java 的四种引用:强引用、软引用、弱引用、虚引用
android·kotlin·android jetpack
恋猫de小郭6 小时前
Flutter Golden Tests:给 AI Agent 的 UI 测试系统
android·前端·flutter
知昂七昂6 小时前
AOSP编译:敲下 `lunch xxx-userdebug`,你知道系统偷偷干了什么吗?
android
千里马学框架6 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation