Android画中画(PIP)全解析,踩坑记录

1,011 阅读5分钟

前言

在公司主要负责直播模块的开发,前段时间来了个需求直播间要加入小窗口播放功能,一开始是打算使用悬浮窗来做的,但是由于悬浮窗要申请权限,转而使用了画中画来实现,没想到从此一脚踩进里深坑。好在经过一系列的适配总算是勉强达到了预期效果,时隔一月,打算写一篇踩坑记录来帮助(劝退)后来的小伙子,我的建议是能不用画中画就尽量不用画中画,它有一些无法避免的问题。

PIP介绍

pip模式简而言之就是可以让你的activity缩小,像一个悬浮窗一样浮在所有应用上层,并且不需要用户授权,权限是默认开启的,主要用于直播或者视频详情页等页面,在api24以上开始支持

官方文档 developer.android.google.cn/guide/topic…

注意事项

  • 进入pip模式的Activity上面会有一层系统加上去的控制层,可以添加一些控制控件,属于remoteView,而且控件位置无法自定义设置,对于不同的ROM它们的位置和UI都会有差异

  • activity进入画中画模式以后会处于onPause状态,所以对于播放器的播放暂停控制不能放在onPause/onResume方法里面

  • 画中画只是根据比例将activity缩小了,所以进入/退出画中画模式时要对ui做出对应调整,比如显示/隐藏某些控件,需要在activity/fragment的onPictureInPictureModeChanged回调中进行处理

  • activity进入画中画模式后会从当前task剥离,进入一个单独的task,导致任务管理器展示两个task

  • 画中画的宽高比有限制,超出会抛出异常

    image.png

除此之外还有一个更严重的问题,从A(非PIP页面)跳转到B(PIP页面)然后进入PIP,此时B窗口化浮在顶层,再从A页面点击跳到C页面,如果C页面是singleTask(或者跳转的时候intent flag加了 new_task),C页面就会错误地在B页面的task中以画中画模式打开。有理由认为这是一个bug,C页面明明没有设置taskAffinity却进入了另一个task(B页面的task)而不是默认task(A页面的task),下面我们来单独分析一下这个问题。

PIP模式下的栈管理

PIP模式默认行为

首先我们不给需要适配PIP模式的activity声明任何task相关的属性,从A页面(LessonEntryActivity) 跳转到 PIP页面(LiveRoomListActivity),先不进入PIP模式,dump一下task信息,可以看到两个页面都在一个task中,taskId 256

image.png

然后我们让LiveRoomListActivity进入PIP模式,再dump一下,可以看到此时两个页面在不同的task中了

image.png

image.png

然后我们再从LessonEntryActivity进入一个别的页面(LessonDetailActivity),这个页面是singleTask的,可以看到它在LiveRoomListActivity的task中以PIP模式打开了,现象和dump信息一致

image.png

这样的结果是无法接受的,并且没有办法解决,因为你不能保证所有从A页面跳转的页面都不是singleTask或者没添加new_task flag

PIP页面设为SingleInstance

既然singleTask会让别的页面进入PIP页面的task导致显示异常,那么把PIP页面的launchMode设为singleInstance不就能规避这个问题吗。

虽然这个方法是可以解决显示异常的问题,但是它带来的副作用也不容小觑

  • 一进入PIP页面就会进入一个新task,尽管还没有进入PIP模式
  • 从PIP页面进入一个singleTask页面后无法返回PIP页面,只能返回到PIP页面的上个页面

这两个问题就会让用户体验变得比较差

更好的方法

在观察了腾讯视频的相同功能后我在想肯定还有更好的办法能做到和它相同的效果,于是我想到了taskAffinity,自己来管理PIP页面的task

  1. 为PIP页面添加一个taskAffinity

    如果设置了PIP页面的taskAffinity,进入PIP模式后系统会为你创建对应的task并把页面放入这个task,只要保证它的affinity和其他的activity都不同,就可以让别的activity无论如何都进不了这个task,规避第一个问题

  2. PIP页面launchMode保持standard,保证跳转到PIP页面的intent中没有FLAG_ACTIVITY_NEW_TASK

    即使给PIP页面设置了taskAffinity,在没有将它设为singleTask/singleInstance或者跳转添加FLAG_ACTIVITY_NEW_TASK时,进入该页面都不会创建并且进入新的task,只有当进入了PIP模式以后才会创建并且并进入新的task,在一定程度上规避了第二问题

  3. 启动PIP页面的时候添加FLAG_ACTIVITY_CLEAR_TOP让它重建达到singleTask的效果,同时跳转前需要关闭掉可能存在于其他task的PIP页面实例

    这个处理是为了在没有设置singleTask的情况下保持PIP页面的唯一性,可以根据业务需求决定要不要加 由于进入PIP模式以后activity会进入别的task,这种情况下FLAG_ACTIVITY_CLEAR_TOP并不能关掉它,需要一些额外处理

    if (ctx != null) {
        if (ctx is LiveRoomListActivity && ctx.isTaskRoot) {
            // 进入过画中画,已经在单独的栈,在PIP页面里面再跳转PIP
        } else {
            // 进PIP页面前先关掉在别的task的PIP页面实例,finish整个task
            val activityManager =
                (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)
            val appTasks = activityManager.appTasks
            for (task in appTasks) {
                val baseIntent = task.taskInfo.baseIntent
                if (baseIntent.component?.className == LiveRoomListActivity::class.java.name) {
                    task.finishAndRemoveTask()
                    return
                }
            }
        }
        ctx.safeStartActivity(intent)
    }
    

经过这些处理总算达到了较为理想的状态

  • 在进入pip页面时并不会跳转到别的task,进入pip模式后才会开启新的task(即使退出pip模式还是处于新的task,无法处理)
  • 在别的页面再次进入pip页面会将之前的pip页面销毁(无论是不是在别的task)

这样一来双task同时存在的场景减少了很多

其他问题

在不同的ROM下还有一些兼容性问题,比如小米手机有时候onPictureInPictureModeChanged回调不正确,ov手机切换深色模式的影响等。

总之如果要适配PIP多的是坑,google issue tracker上面对于相关问题也没有进度,如果没有硬性需求建议还是绕道走不要用了