您可以在应用层调用播放器SDK,结合系统API实现悬浮窗、画中画播放功能。本文为您介绍如何在Android和iOS平台上实现播放器SDK的悬浮窗、画中画播放功能。
Android技术方案
Android系统提供了多种不同的方案供您选择,包括悬浮窗、画中画方案,例如电商场景下常见的浮窗方案。此处仅介绍常见的实现方法。
悬浮窗
悬浮窗是Android系统中的一种浮动窗口,可以在其他应用程序的上层显示。它可以进行随意拖动、缩放、关闭等操作,通常用于提醒、通知和广告。
在Android系统中,每个窗口都对应一个Window对象,而悬浮窗就是一种特殊的Window,通常情况下,悬浮窗可以通过ViewSystem中的PopupWindow来实现。
其中,PopupWindow是继承自具有运动能力的WindowManager.LayoutParams的一个类,因此可以对其进行位置、大小和显示方式等操作。同时,可以使用PopupWindow实现一个自定义悬浮窗,而不影响其他应用,并且无需通过Activity跳转。
实现步骤如下:
在AndroidManifest.xml中声明悬浮窗权限。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
在需要显示悬浮窗的Activity或Service中,通过创建WindowManager和PopupWindow,设置其显示位置、大小和内容等属性。
//创建布局 View layout = View.inflate(this, R.layout.float_window, null); //创建管理器 WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE); //创建悬浮窗 PopupWindow mPopupWindow = new PopupWindow(layout, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); //设置显示位置 mPopupWindow.showAtLocation(parentView, Gravity.LEFT | Gravity.TOP, x, y); //设置可点击和可获取焦点 mPopupWindow.setTouchable(true); mPopupWindow.setFocusable(true); //设置背景色 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
设置悬浮窗布局内控件的点击事件、拖拽监听事件等。
mPopupWindow.setOnTouchListener(new View.OnTouchListener() { int lastX, lastY; int paramX, paramY; @Override public boolean onTouch(View v, MotionEvent event) { //获取当前触摸点相对于屏幕的坐标 int x = (int) event.getRawX(); int y = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = x; lastY = y; paramX = mPopupWindow.getLayoutParams().x; paramY = mPopupWindow.getLayoutParams().y; break; case MotionEvent.ACTION_MOVE: int dx = x - lastX; int dy = y - lastY; mPopupWindow.update(paramX + dx, paramY + dy, -1, -1); break; } return false; } });
重要在Android 6.0及以上版本中,需要动态申请悬浮窗权限。然而,即使如此,在某些Android 8.0手机上,仍然不支持此功能。
画中画
从Android 8.0(API 26)开始,Android引入了画中画(PiP)模式启动Activity的功能。画中画是一种特殊类型的多窗口模式,最常用于视频播放。在该模式下,用户可以将视频以小窗口的形式固定在屏幕的一角,同时在应用之间进行导航或浏览主屏幕上的内容。
画中画利用Android 7.0中提供的多窗口模式API来实现固定的视频叠加窗口。为了在应用中添加画中画功能,您需要注册支持画中画的Activity,并根据需要将该Activity切换为画中画模式,同时,确保当Activity处于画中画模式时,界面元素处于隐藏状态且视频能够继续播放。
画中画窗口会显示在屏幕的最上层,通常位于系统选择的一角。
有关Android系统对画中画功能的支持情况请参见:Android · 对画中画 (PiP) 的支持。
实现步骤如下:
在AndroidManifest.xml中声明Activity对画中画的支持。
<Activity android:name="VideoActivity" android:supportsPictureInPicture="true" android:configChanges= "screenSize|smallestScreenSize|screenLayout|orientation" ...
默认情况下,Android系统不会自动为应用提供画中画功能的支持。如果您想在应用中支持画中画功能,可以在AndroidManifest.xml清单文件中注册视频Activity,并将android:supportsPictureInPicture属性设置为true。
另外,在注册支持画中画的Activity时,还需要指定该Activity来处理布局配置更改。这样,在画中画模式切换期间,如果出现布局更改,您的Activity将不会重新启动,从而提供更加流畅的用户体验。
重要低RAM设备可能无法使用画中画模式。在应用使用画中画之前,请务必通过调用hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)进行检查,以确保可以使用画中画。
将Activity切换到画中画模式。
进入PIP模式的最常见流程如下:
从按钮触发(例如通过点击按钮)
onClicked(View),onOptionsItemSelected(MenuItem) 等等。
@Override public void onActionClicked(Action action) { if (action.getId() == R.id.lb_control_picture_in_picture) { // 从按钮触发 enterPictureInPictureMode(); return; } ... }
有意的离开您的应用程序触发(例如通过按下Home键)
onUserLeaveHint()
如要进入画中画模式,Activity必须调用enterPictureInPictureMode()。在用户按下主屏幕或最近使用的应用按钮时,可以通过替换onUserLeaveHint()来实现应用自动切换到画中画模式。
@Override public void onUserLeaveHint () { // 有意的离开您的应用程序触发 if (iWantToBeInPipModeNow()) { enterPictureInPictureMode(); } }
从返回触发(例如通过按下返回按钮)
onBackPressed()
@Override public void onBackPressed() { super.onBackPressed(); // 从返回触发 enterPictureInPictureMode(); }
处理画中画模式下的界面元素。
当Activity进入或退出画中画模式时,系统会调用Activity.onPictureInPictureModeChanged()方法或Fragment.onPictureInPictureModeChanged()方法。
您应替换这些回调以重新绘制Activity的界面元素。请注意,在画中画模式下,Activity会以一个小窗口的形式显示,用户无法与应用的界面元素进行互动,并且可能难以看清小窗口中的详细信息。界面极简的视频播放Activity可提供最佳的用户体验。
退出画中画模式时支持更流畅的动画。
当Activity退出画中画模式时,您可以为界面元素添加退出动画,以提升过渡的流畅性和视觉效果。
添加控件。
在画中画模式下,用户可能无法与应用的界面元素进行互动。因此,您可以考虑在画中画窗口中添加一些简单的控件,以便用户可以进行基本的操作,如播放/暂停、上/下一集等。
为非视频内容停用无缝大小调整。
在画中画模式下,视频内容通常需要保持适当的大小比例,不被拉伸或裁剪。然而,对于非视频内容,如文本、图像等,可能需要禁用无缝大小调整,以避免内容在小窗口中变得不可读或失真。通过禁用无缝大小调整,可以确保非视频内容在画中画模式下保持良好的可见性和可操作性。
在画中画模式下继续播放视频。
当Activity切换到画中画模式时,系统会将该Activity置于暂停状态并调用Activity的onPause()方法。然而,在画中画模式下,视频播放应该继续进行,而不是暂停。
在Android 7.0及更高版本中
当系统调用Activity的onStop()时,您应暂停视频播放。
当系统调用Activity的onStart()时,您应恢复视频播放。
这样,您就无需在onPause()方法中检查应用是否处于画中画模式,只需继续播放视频即可。
如果您必须在onPause()方法中暂停视频播放,请通过调用isInPictureInPictureMode()方法来检查是否处于画中画模式,并根据需要相应地处理播放状态。以下是一个示例代码:
@Override public void onPause() { // If called while in PiP mode, do not pause playback if (isInPictureInPictureMode()) { // Continue playback ... } else { // Use existing playback logic for paused Activity behavior. ... } }
iOS技术方案
目前有三种方式可以实现画中画功能:
WKWebView自带
如果您在应用中使用了WKWebView进行视频播放,它已经内置了画中画功能。
使用AVPlayerViewController
如果对播放器的要求不是很高,可以直接使用AVPlayerViewController。它已经提供了画中画功能,只需设置allowsPictureInPicturePlayback属性为YES,即可在播放器界面上展示画中画按钮。
自定义播放器并使用AVPictureInPictureController包装
如果您使用自定义的播放器,并希望开启画中画功能,可以使用AVPictureInPictureController对播放器进行包装,简单易用地实现画中画功能,并且AVPictureInPictureController内部已经实现了动画效果。只需注意用户需要自己实现画中画按钮,系统已提供了相关API(pictureInPictureButtonStartImage)来使用画中画图标。
悬浮窗
简单理解为,可以利用UIWindow类型创建一个新的窗口,并将视频播放器的视图添加到该窗口上,通过手势控制窗口的位置和大小,实现悬浮的画中画效果。
在iOS的设计准则中,明确规定了不允许在应用程序中使用悬浮窗。使用悬浮窗可能会被苹果拒绝审核或被迫下架。同时也要考虑到用户体验和隐私保护问题,因此在使用悬浮窗时需要谨慎考虑。
画中画
画中画(Picture-in-Picture)功能在iOS 9版本就已经推出,但在之前的版本中,该功能只能在iPad上使用。直到iOS 14版本,iPhone用户才能开始使用画中画功能。
画中画功能在iOS上有两种实现方案:
支持iOS 14以上版本的老方案。
在iOS 14系统中,通过使用系统提供的AVPlayer来初始化AVPictureInPictureController, 进而实现在应用程序压后台或进入二级页面时出现画中画效果。这种方案适用于对播放器要求不太高的场景。
支持iOS 15版本以上的新方案。
在iOS 15及更高版本中,可以使用SamplebufferLayer来创建AVPictureInPictureController,以实现无缝的画中画播放。这种方案通常用于自定义播放器来实现画中画功能。目前,阿里云播放器SDK已提供画中画功能,操作详情请参见画中画。
实现步骤如下:
iOS 15与iOS 14方案实现基本类似,本文以iOS 14老方案为例:
开启后台模式
导入框架#import <AVKit/AVKit.h>创建AVPictureInPictureController。
//1.判断是否支持画中画功能 if ([AVPictureInPictureController isPictureInPictureSupported]) { //2.开启权限 @try { NSError *error = nil; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionOrientationBack error:&error]; [[AVAudioSession sharedInstance] setActive:YES error:&error]; } @catch (NSException *exception) { NSLog(@"AVAudioSession发生错误"); } self.pipVC = [[AVPictureInPictureController alloc] initWithPlayerLayer:self.player]; self.pipVC.delegate = self; }
开启或关闭画中画。
if (self.pipVC.isPictureInPictureActive) { [self.pipVC stopPictureInPicture]; } else { [self.pipVC startPictureInPicture]; }
代理AVPictureInPictureControllerDelegate。
// 即将开启画中画 - (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController; // 已经开启画中画 - (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController; // 开启画中画失败 - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error; // 即将关闭画中画 - (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController; // 已经关闭画中画 - (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController; // 关闭画中画且恢复播放界面 - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL restored))completionHandler;
重要通过一个全局变量持有画中画控制器,可以在pictureInPictureControllerWillStartPictureInPicture持有,在pictureInPictureControllerDidStopPictureInPicture释放;
有时可能不是通过点击画中画按钮,而是通过其他途径来打开当前的画中画控制器。可以在viewWillAppear方法中进行判断并关闭。
在已经存在画中画的情况下,若要开启新的画中画,需等待完全关闭之后再进行新的开启,以防止出现未知错误,因为关闭画中画是一个有过程的操作。
在创建AVPictureInPictureController并同时开启画中画功能时,可能会出现失效的情况。如果遇到这种情况,建议延迟开启画中画功能即可。