您可以在應用層調用播放器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并同時開啟畫中畫功能時,可能會出現失效的情況。如果遇到這種情況,建議延遲開啟畫中畫功能即可。