如何实现悬浮窗和画中画功能

您可以在应用层调用播放器SDK,结合系统API实现悬浮窗、画中画播放功能。本文为您介绍如何在Android和iOS平台上实现播放器SDK的悬浮窗、画中画播放功能。

Android技术方案

Android系统提供了多种不同的方案供您选择,包括悬浮窗、画中画方案,例如电商场景下常见的浮窗方案。此处仅介绍常见的实现方法。

悬浮窗

悬浮窗是Android系统中的一种浮动窗口,可以在其他应用程序的上层显示。它可以进行随意拖动、缩放、关闭等操作,通常用于提醒、通知和广告。

  • 在Android系统中,每个窗口都对应一个Window对象,而悬浮窗就是一种特殊的Window,通常情况下,悬浮窗可以通过ViewSystem中的PopupWindow来实现。

  • 其中,PopupWindow是继承自具有运动能力的WindowManager.LayoutParams的一个类,因此可以对其进行位置、大小和显示方式等操作。同时,可以使用PopupWindow实现一个自定义悬浮窗,而不影响其他应用,并且无需通过Activity跳转。

实现步骤如下:

  1. 在AndroidManifest.xml中声明悬浮窗权限。

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
  2. 在需要显示悬浮窗的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));
  3. 设置悬浮窗布局内控件的点击事件、拖拽监听事件等。

    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) 的支持

实现步骤如下:

  1. 在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)进行检查,以确保可以使用画中画。

  2. 将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();
      }
  3. 处理画中画模式下的界面元素。

    当Activity进入或退出画中画模式时,系统会调用Activity.onPictureInPictureModeChanged()方法或Fragment.onPictureInPictureModeChanged()方法。

    您应替换这些回调以重新绘制Activity的界面元素。请注意,在画中画模式下,Activity会以一个小窗口的形式显示,用户无法与应用的界面元素进行互动,并且可能难以看清小窗口中的详细信息。界面极简的视频播放Activity可提供最佳的用户体验。

    1. 退出画中画模式时支持更流畅的动画。

      当Activity退出画中画模式时,您可以为界面元素添加退出动画,以提升过渡的流畅性和视觉效果。

    2. 添加控件。

      在画中画模式下,用户可能无法与应用的界面元素进行互动。因此,您可以考虑在画中画窗口中添加一些简单的控件,以便用户可以进行基本的操作,如播放/暂停、上/下一集等。

    3. 为非视频内容停用无缝大小调整。

      在画中画模式下,视频内容通常需要保持适当的大小比例,不被拉伸或裁剪。然而,对于非视频内容,如文本、图像等,可能需要禁用无缝大小调整,以避免内容在小窗口中变得不可读或失真。通过禁用无缝大小调整,可以确保非视频内容在画中画模式下保持良好的可见性和可操作性。

  4. 在画中画模式下继续播放视频。

    当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技术方案

目前有三种方式可以实现画中画功能:

  1. WKWebView自带

    如果您在应用中使用了WKWebView进行视频播放,它已经内置了画中画功能。

  2. 使用AVPlayerViewController

    如果对播放器的要求不是很高,可以直接使用AVPlayerViewController。它已经提供了画中画功能,只需设置allowsPictureInPicturePlayback属性为YES,即可在播放器界面上展示画中画按钮。

  3. 自定义播放器并使用AVPictureInPictureController包装

    如果您使用自定义的播放器,并希望开启画中画功能,可以使用AVPictureInPictureController对播放器进行包装,简单易用地实现画中画功能,并且AVPictureInPictureController内部已经实现了动画效果。只需注意用户需要自己实现画中画按钮,系统已提供了相关API(pictureInPictureButtonStartImage)来使用画中画图标。

悬浮窗

简单理解为,可以利用UIWindow类型创建一个新的窗口,并将视频播放器的视图添加到该窗口上,通过手势控制窗口的位置和大小,实现悬浮的画中画效果。

重要

在iOS的设计准则中,明确规定了不允许在应用程序中使用悬浮窗。使用悬浮窗可能会被苹果拒绝审核或被迫下架。同时也要考虑到用户体验和隐私保护问题,因此在使用悬浮窗时需要谨慎考虑。

画中画

画中画(Picture-in-Picture)功能在iOS 9版本就已经推出,但在之前的版本中,该功能只能在iPad上使用。直到iOS 14版本,iPhone用户才能开始使用画中画功能。

画中画功能在iOS上有两种实现方案:

  1. 支持iOS 14以上版本的老方案。

    image

    在iOS 14系统中,通过使用系统提供的AVPlayer来初始化AVPictureInPictureController, 进而实现在应用程序压后台或进入二级页面时出现画中画效果。这种方案适用于对播放器要求不太高的场景。

  2. 支持iOS 15版本以上的新方案。

    image

    在iOS 15及更高版本中,可以使用SamplebufferLayer来创建AVPictureInPictureController,以实现无缝的画中画播放。这种方案通常用于自定义播放器来实现画中画功能。目前,阿里云播放器SDK已提供画中画功能,操作详情请参见画中画

实现步骤如下:

iOS 15与iOS 14方案实现基本类似,本文以iOS 14老方案为例:

  1. 开启后台模式

    image

  2. 导入框架#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;
    }
  3. 开启或关闭画中画。

    if (self.pipVC.isPictureInPictureActive) {
        [self.pipVC stopPictureInPicture];
    } else {
        [self.pipVC startPictureInPicture];
    }
  4. 代理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;
    重要
    1. 通过一个全局变量持有画中画控制器,可以在pictureInPictureControllerWillStartPictureInPicture持有,在pictureInPictureControllerDidStopPictureInPicture释放;

    2. 有时可能不是通过点击画中画按钮,而是通过其他途径来打开当前的画中画控制器。可以在viewWillAppear方法中进行判断并关闭。

    3. 在已经存在画中画的情况下,若要开启新的画中画,需等待完全关闭之后再进行新的开启,以防止出现未知错误,因为关闭画中画是一个有过程的操作。

    4. 在创建AVPictureInPictureController并同时开启画中画功能时,可能会出现失效的情况。如果遇到这种情况,建议延迟开启画中画功能即可。