Android 原生 Picture in Picture 画中画功能避坑指南(上)

简介: Android 原生 Picture in Picture 画中画功能避坑指南(上)

最近生活有些变动所以断更好久,不过虽迟到但永远不会缺席。ChatGPT 浪潮还在持续扩大,各位同学一定要体验体验丫~

这篇主要介绍最近需求中遇到的问题,希望能帮助后来者少踩坑。先说结论:Android 原生画中画功能并不完善,如果可以接受 APP 有两个任务栈则可以使用;否则趁早自己用浮窗自定义实现画中画的功能吧。


1. PiP 简介


Android PiP 模式也称之为画中画模式,允许用户在使用应用程序的同时,在屏幕的一角或一侧浮动显示另一个应用程序或视频。这使得用户可以同时进行多项任务,而不必切换应用程序或中断正在进行的任务。如下所示:

image.png

(注:B站的 PiP 是自定义实现的,未使用系统 PiP)


2. 准备工作,跑通 Demo


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

官方Demo:github.com/android/med…

打开官方 Demo,首先得改一下 minSdkVersion,demo 里设置的是 API 31(Android 12.0),不满足实际应用需求,这里改为 23(Android 6.0). 但 PiP 功能只能在 Android8.0 及以上的系统上使用,所以用到一些方法时,需要注明 @RequiresApi(Build.VERSION_CODES.O)。所以,如果需要在 Android 8.0 以下的设备支持 PiP,只能使用自定义悬浮窗实现

还需要注释掉 setAutoEnterEnabled(true)setSeamlessResizeEnabled(false) 这两个方法。因为它们只能在 Android 12.0 及以上系统使用,且对于 PiP 的主体功能没有影响。setAutoEnterEnabled 用于设置 Activity 在退到后台时是否自动进入 PiP 模式,当设置为 true,则在用户点击 Home 键回到主屏幕时,Activity 可自动进入 PiP 模式,而不用开发者手动调用 enterPictureInPictureMode 方法;setSeamlessResizeEnabled 用于设置非视频画中画时的动画效果,不影响功能。

按照上述的内容设置完后就可以将 Demo 跑通了。


3. 示例代码分析


仅分析查看了 Demo 中的 MovieActivity 中的 PiP 相关的代码。比较重要的代码如下:

// code 1
    @RequiresApi(Build.VERSION_CODES.O)
    private fun minimize() {
        enterPictureInPictureMode(updatePictureInPictureParams())
    }

调用 enterPictureInPictureMode(@NonNull PictureInPictureParams params) 方法就可以进入 PiP,声明如下:

// code 2
    public boolean enterPictureInPictureMode(@NonNull PictureInPictureParams params) { 
    ···
  }

方法简介:它是 Activity 类中的方法,需要传递一个 PictureInPictureParams 类型对象。当系统成功将该 Activity 切换到 PiP 模式或已经处于 PiP,则返回值为 true;如果设备不支持 PiP 则返回 false。

再来看下构建 PictureInPictureParams 类型对象的 updatePictureInPictureParams() 方法:

// code 3
@RequiresApi(Build.VERSION_CODES.O)
    private fun updatePictureInPictureParams(): PictureInPictureParams {
        // 1、计算出 PiP 小窗的宽高比,这里直接使用播放视频的控件宽和高计算
        val aspectRatio = Rational(binding.movie.width, binding.movie.height)
        // 2、将播放视频的控件binding.movie设置为 PiP 中要展示的部分
        val visibleRect = Rect()
        binding.movie.getGlobalVisibleRect(visibleRect)
        val params = PictureInPictureParams.Builder()
            .setAspectRatio(aspectRatio)
            // 3、指定进入画中画的屏幕部分。系统根据这个可实现平滑动画效果。这里就把之前生成的 visibleRect 传值过去
            .setSourceRectHint(visibleRect)
            .build()
        setPictureInPictureParams(params)
        return params
    }

updatePictureInPictureParams 方法作用是构建出进入 PiP 的一些参数,比如进入小窗的控件,小窗的宽高比等。注释很清楚,源码直接拿来套用就行。需要注意的点:只能指定 PiP 模式的宽高比,并不能直接设置宽和高的具体值,系统会根据设置的宽高比自己计算具体值。

如果在播放器控件上层有其他的操作按钮等,还需要在 onPictureInPictureModeChanged 回调中进行处理,即进入 PiP 后隐藏这些按钮;退出后恢复这些按钮的状态。 如下是 Demo 中的实现:

// code 4
    override fun onPictureInPictureModeChanged(
        isInPictureInPictureMode: Boolean, newConfig: Configuration
    ) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
        if (isInPictureInPictureMode) {
            // Hide the controls in picture-in-picture mode.
            binding.movie.hideControls()
        } else {
            // Show the video controls if the video is not playing
            if (!binding.movie.isPlaying) {
                binding.movie.showControls()
            }
        }
    }

通过这个方法可以监听 PiP 的进入和退出。

还有一些是 PiP 模式下的播放/暂停、上一个/下一个 操作按钮,即下图红框中的这三个按钮,相关的使用方式 Demo 中已有示例,这里不再赘述。

image.png

除此之外,还要在需要进入 PiP 的 Activity 的 AndroidManifest 中设置支持 PiP 的属性以及处理布局配置更改。这样一来,如果在 PiP 模式转换期间出现布局更改,该 Activity 就不会重新启动。

// code 5
<activity android:name="VideoActivity"
    android:supportsPictureInPicture="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
    ...


4. 功能实现及踩坑汇总


4.1 实现点击 Back 键及 Home 键自动进入 PiP


用户在观看视频时,点击返回键或 Home 键,当前 Activity 需要进入 PiP 继续播放,这是个常见的功能,实现起来也比较简单:

// code 6
    // 实现点击返回键进入 PiP
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onBackPressed() {
        enterPictureInPictureMode(updatePictureInPictureParams())
    }
    // 实现点击 Home 键进入 PiP
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onUserLeaveHint() {
        super.onUserLeaveHint()
        enterPictureInPictureMode(updatePictureInPictureParams())
    }

如果设置了之前提到的 setAutoEnterEnabled(true) 方法,则可以不用在 onUserLeaveHint() 回调里主动调用 enterPictureInPictureMode 方法进入 PiP。但建议还是不用 setAutoEnterEnabled,因为它只能在 Android 12 上使用。。。

onUserLeaveHint() 方法也是 Activity 中的方法,当 Activity 进入后台时就会调用它,比如用户点击 Home 键就会回调它。但有来电时,来电的 Activity 会自动带到前台,这时被退到后台的 Activity 的 onUserLeaveHint 方法并不会被调用。onUserLeaveHint 的调用时机是在 onPause 方法之前,这点需要注意。


4.2 实现 Activity 处于 PiP 时再次进入更新视频


假设 MovieActivity 已处于 PiP 并正在播放视频,用户点击另外一个视频又要跳转到 MovieActivity 的情形。如果不进行处理就会出现有两个 MovieActivity 同时播放视频的情况,即小窗播放的同时,还有一个另一个 MovieActivity 也在播放。如下所示,本来只有一个 PiP 在播放视频,然后点击 WATCH VIDEO TWO 按钮又进入了 MovieActivity,此时有两个视频同时在播放:

image.png

查看堆栈信息确实有两个 MovieActivity:

image.png

这种情况下是需要将 MovieActivity 由 PiP 恢复到正常状态并播放新的视频,如果视频内容没有变则接着播放原视频。官方 Demo 也有说明如何处理,需要两个步骤:

1)将 MovieActivity 的 launchMode 设置为 singleTask

2)在 MovieActivity 的 onNewIntent 方法里处理更新数据等逻辑;

比如我在打开 MovieActivity 时通过 Intent 传递不同的 video 来播放不同的视频,那么在 onNewIntent 中就需要接收传递的参数并更新:

// code 7
// MainActivity.kt    通过 Intent 传入不同的视频
        binding.btnWatchVid1.setOnClickListener {
            val intent = Intent(this, MovieActivity::class.java)
            intent.putExtra(MovieActivity.KEY_VIDEO_ID, R.raw.vid_bigbuckbunny)
            startActivity(intent)
        }
        binding.btnWatchVid2.setOnClickListener {
            val intent = Intent(this, MovieActivity::class.java)
            intent.putExtra(MovieActivity.KEY_VIDEO_ID, R.raw.vid_dajiang)
            startActivity(intent)
        }
// code 8
// MovieActivity.kt    onNewIntent 接收并更新
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        val newVideoId = intent?.getIntExtra(KEY_VIDEO_ID, R.raw.vid_bigbuckbunny)
        newVideoId?.let {
          // 更新视频
            binding.movie.setVideoResourceId(it)
        }
    }

在实际中可能更加复杂,但大体思路是一致的。

目录
相关文章
|
23天前
|
Android开发
错误记录:调用原生TvSettings 的 com.android.tv.settings.device.storage.ResetActivity 无法启动
本文记录了一个Android TV设置中由于未设置`android:exported="true"`导致`com.android.tv.settings.device.storage.ResetActivity`无法被第三方app启动的错误,并通过添加该属性成功解决了问题。
32 1
|
29天前
|
编解码 测试技术 Android开发
Android经典实战之用 CameraX 库实现高质量的照片和视频拍摄功能
本文详细介绍了如何利用CameraX库实现高质量的照片及视频拍摄功能,包括添加依赖、初始化、权限请求、配置预览与捕获等关键步骤。此外,还特别针对不同分辨率和帧率的视频拍摄提供了性能优化策略,确保应用既高效又稳定。
62 1
Android经典实战之用 CameraX 库实现高质量的照片和视频拍摄功能
|
1月前
|
图形学 Android开发
小功能⭐️Unity调用Android常用事件
小功能⭐️Unity调用Android常用事件
|
3月前
|
数据库 Android开发 数据安全/隐私保护
在 Android Studio 中结合使用 SQLite 数据库实现简单的注册和登录功能
在 Android Studio 中结合使用 SQLite 数据库实现简单的注册和登录功能
157 2
|
3月前
|
Android开发
Android中如何快速的实现RecycleView的拖动重排序功能
使用`ItemTouchHelper`和自定义`Callback`,在`RecyclerView`中实现拖动排序功能。定义`ItemTouchHelperAdapter`接口,`Adapter`实现它以处理`onItemMove`方法。`SimpleItemTouchHelperCallback`设置拖动标志,如`LEFT`或`RIGHT`(水平拖动),并绑定到`RecyclerView`以启用拖动。完成这些步骤后,即可实现拖放排序。关注公众号“AntDream”获取更多内容。
79 3
|
4月前
|
移动开发 监控 Android开发
构建高效Android应用:从内存优化到电池寿命代码之美:从功能实现到艺术创作
【5月更文挑战第28天】 在移动开发领域,特别是针对Android系统,性能优化始终是关键议题之一。本文深入探讨了如何通过细致的内存管理和电池使用策略,提升Android应用的运行效率和用户体验。文章不仅涵盖了现代Android设备上常见的内存泄漏问题,还提出了有效的解决方案,包括代码级优化和使用工具进行诊断。同时,文中也详细阐述了如何通过减少不必要的后台服务、合理管理设备唤醒锁以及优化网络调用等手段延长应用的电池续航时间。这些方法和技术旨在帮助开发者构建更加健壮、高效的Android应用程序。
|
3月前
|
存储 数据库 Android开发
在 Android Studio 中结合使用 SQLite 数据库实现简单的注册和登录功能
在 Android Studio 中结合使用 SQLite 数据库实现简单的注册和登录功能
82 0
|
4月前
|
Android开发 数据安全/隐私保护 iOS开发
ios和安卓测试包发布网站http://fir.im的注册与常用功能
ios和安卓测试包发布网站http://fir.im的注册与常用功能
120 0
ios和安卓测试包发布网站http://fir.im的注册与常用功能
|
4月前
|
API 开发工具 Android开发
调用Android原生@SystemApi、@Hide方法
调用Android原生@SystemApi、@Hide方法
357 1
|
4月前
|
Android开发
Android SystemUI去掉拖动亮度条QSPanel界面隐藏功能
Android SystemUI去掉拖动亮度条QSPanel界面隐藏功能
85 0