最近生活有些变动所以断更好久,不过虽迟到但永远不会缺席。ChatGPT 浪潮还在持续扩大,各位同学一定要体验体验丫~
这篇主要介绍最近需求中遇到的问题,希望能帮助后来者少踩坑。先说结论:Android 原生画中画功能并不完善,如果可以接受 APP 有两个任务栈则可以使用;否则趁早自己用浮窗自定义实现画中画的功能吧。
1. PiP 简介
Android PiP 模式也称之为画中画模式,允许用户在使用应用程序的同时,在屏幕的一角或一侧浮动显示另一个应用程序或视频。这使得用户可以同时进行多项任务,而不必切换应用程序或中断正在进行的任务。如下所示:
(注: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 中已有示例,这里不再赘述。
除此之外,还要在需要进入 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,此时有两个视频同时在播放:
查看堆栈信息确实有两个 MovieActivity:
这种情况下是需要将 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) } }
在实际中可能更加复杂,但大体思路是一致的。