如何實現(xiàn)懸浮窗和畫中畫功能
您可以在應(yīng)用層調(diào)用播放器SDK,結(jié)合系統(tǒng)API實現(xiàn)懸浮窗、畫中畫播放功能。本文為您介紹如何在Android和iOS平臺上實現(xiàn)播放器SDK的懸浮窗、畫中畫播放功能。
Android技術(shù)方案
Android系統(tǒng)提供了多種不同的方案供您選擇,包括懸浮窗、畫中畫方案,例如電商場景下常見的浮窗方案。此處僅介紹常見的實現(xiàn)方法。
懸浮窗
懸浮窗是Android系統(tǒng)中的一種浮動窗口,可以在其他應(yīng)用程序的上層顯示。它可以進行隨意拖動、縮放、關(guān)閉等操作,通常用于提醒、通知和廣告。
在Android系統(tǒng)中,每個窗口都對應(yīng)一個Window對象,而懸浮窗就是一種特殊的Window,通常情況下,懸浮窗可以通過ViewSystem中的PopupWindow來實現(xiàn)。
其中,PopupWindow是繼承自具有運動能力的WindowManager.LayoutParams的一個類,因此可以對其進行位置、大小和顯示方式等操作。同時,可以使用PopupWindow實現(xiàn)一個自定義懸浮窗,而不影響其他應(yīng)用,并且無需通過Activity跳轉(zhuǎn)。
實現(xiàn)步驟如下:
在AndroidManifest.xml中聲明懸浮窗權(quán)限。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
在需要顯示懸浮窗的Activity或Service中,通過創(chuàng)建WindowManager和PopupWindow,設(shè)置其顯示位置、大小和內(nèi)容等屬性。
//創(chuàng)建布局 View layout = View.inflate(this, R.layout.float_window, null); //創(chuàng)建管理器 WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE); //創(chuàng)建懸浮窗 PopupWindow mPopupWindow = new PopupWindow(layout, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); //設(shè)置顯示位置 mPopupWindow.showAtLocation(parentView, Gravity.LEFT | Gravity.TOP, x, y); //設(shè)置可點擊和可獲取焦點 mPopupWindow.setTouchable(true); mPopupWindow.setFocusable(true); //設(shè)置背景色 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
設(shè)置懸浮窗布局內(nèi)控件的點擊事件、拖拽監(jiān)聽事件等。
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及以上版本中,需要動態(tài)申請懸浮窗權(quán)限。然而,即使如此,在某些Android 8.0手機上,仍然不支持此功能。
畫中畫
從Android 8.0(API 26)開始,Android引入了畫中畫(PiP)模式啟動Activity的功能。畫中畫是一種特殊類型的多窗口模式,最常用于視頻播放。在該模式下,用戶可以將視頻以小窗口的形式固定在屏幕的一角,同時在應(yīng)用之間進行導(dǎo)航或瀏覽主屏幕上的內(nèi)容。
畫中畫利用Android 7.0中提供的多窗口模式API來實現(xiàn)固定的視頻疊加窗口。為了在應(yīng)用中添加畫中畫功能,您需要注冊支持畫中畫的Activity,并根據(jù)需要將該Activity切換為畫中畫模式,同時,確保當Activity處于畫中畫模式時,界面元素處于隱藏狀態(tài)且視頻能夠繼續(xù)播放。
畫中畫窗口會顯示在屏幕的最上層,通常位于系統(tǒng)選擇的一角。
有關(guān)Android系統(tǒng)對畫中畫功能的支持情況請參見:Android · 對畫中畫 (PiP) 的支持。
實現(xiàn)步驟如下:
在AndroidManifest.xml中聲明Activity對畫中畫的支持。
<Activity android:name="VideoActivity" android:supportsPictureInPicture="true" android:configChanges= "screenSize|smallestScreenSize|screenLayout|orientation" ...
默認情況下,Android系統(tǒng)不會自動為應(yīng)用提供畫中畫功能的支持。如果您想在應(yīng)用中支持畫中畫功能,可以在AndroidManifest.xml清單文件中注冊視頻Activity,并將android:supportsPictureInPicture屬性設(shè)置為true。
另外,在注冊支持畫中畫的Activity時,還需要指定該Activity來處理布局配置更改。這樣,在畫中畫模式切換期間,如果出現(xiàn)布局更改,您的Activity將不會重新啟動,從而提供更加流暢的用戶體驗。
重要低RAM設(shè)備可能無法使用畫中畫模式。在應(yīng)用使用畫中畫之前,請務(wù)必通過調(diào)用hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)進行檢查,以確保可以使用畫中畫。
將Activity切換到畫中畫模式。
進入PIP模式的最常見流程如下:
從按鈕觸發(fā)(例如通過點擊按鈕)
onClicked(View),onOptionsItemSelected(MenuItem) 等等。
@Override public void onActionClicked(Action action) { if (action.getId() == R.id.lb_control_picture_in_picture) { // 從按鈕觸發(fā) enterPictureInPictureMode(); return; } ... }
有意的離開您的應(yīng)用程序觸發(fā)(例如通過按下Home鍵)
onUserLeaveHint()
如要進入畫中畫模式,Activity必須調(diào)用enterPictureInPictureMode()。在用戶按下主屏幕或最近使用的應(yīng)用按鈕時,可以通過替換onUserLeaveHint()來實現(xiàn)應(yīng)用自動切換到畫中畫模式。
@Override public void onUserLeaveHint () { // 有意的離開您的應(yīng)用程序觸發(fā) if (iWantToBeInPipModeNow()) { enterPictureInPictureMode(); } }
從返回觸發(fā)(例如通過按下返回按鈕)
onBackPressed()
@Override public void onBackPressed() { super.onBackPressed(); // 從返回觸發(fā) enterPictureInPictureMode(); }
處理畫中畫模式下的界面元素。
當Activity進入或退出畫中畫模式時,系統(tǒng)會調(diào)用Activity.onPictureInPictureModeChanged()方法或Fragment.onPictureInPictureModeChanged()方法。
您應(yīng)替換這些回調(diào)以重新繪制Activity的界面元素。請注意,在畫中畫模式下,Activity會以一個小窗口的形式顯示,用戶無法與應(yīng)用的界面元素進行互動,并且可能難以看清小窗口中的詳細信息。界面極簡的視頻播放Activity可提供最佳的用戶體驗。
退出畫中畫模式時支持更流暢的動畫。
當Activity退出畫中畫模式時,您可以為界面元素添加退出動畫,以提升過渡的流暢性和視覺效果。
添加控件。
在畫中畫模式下,用戶可能無法與應(yīng)用的界面元素進行互動。因此,您可以考慮在畫中畫窗口中添加一些簡單的控件,以便用戶可以進行基本的操作,如播放/暫停、上/下一集等。
為非視頻內(nèi)容停用無縫大小調(diào)整。
在畫中畫模式下,視頻內(nèi)容通常需要保持適當?shù)拇笮”壤槐焕旎虿眉簟H欢瑢τ诜且曨l內(nèi)容,如文本、圖像等,可能需要禁用無縫大小調(diào)整,以避免內(nèi)容在小窗口中變得不可讀或失真。通過禁用無縫大小調(diào)整,可以確保非視頻內(nèi)容在畫中畫模式下保持良好的可見性和可操作性。
在畫中畫模式下繼續(xù)播放視頻。
當Activity切換到畫中畫模式時,系統(tǒng)會將該Activity置于暫停狀態(tài)并調(diào)用Activity的onPause()方法。然而,在畫中畫模式下,視頻播放應(yīng)該繼續(xù)進行,而不是暫停。
在Android 7.0及更高版本中
當系統(tǒng)調(diào)用Activity的onStop()時,您應(yīng)暫停視頻播放。
當系統(tǒng)調(diào)用Activity的onStart()時,您應(yīng)恢復(fù)視頻播放。
這樣,您就無需在onPause()方法中檢查應(yīng)用是否處于畫中畫模式,只需繼續(xù)播放視頻即可。
如果您必須在onPause()方法中暫停視頻播放,請通過調(diào)用isInPictureInPictureMode()方法來檢查是否處于畫中畫模式,并根據(jù)需要相應(yīng)地處理播放狀態(tài)。以下是一個示例代碼:
@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技術(shù)方案
目前有三種方式可以實現(xiàn)畫中畫功能:
WKWebView自帶
如果您在應(yīng)用中使用了WKWebView進行視頻播放,它已經(jīng)內(nèi)置了畫中畫功能。
使用AVPlayerViewController
如果對播放器的要求不是很高,可以直接使用AVPlayerViewController。它已經(jīng)提供了畫中畫功能,只需設(shè)置allowsPictureInPicturePlayback屬性為YES,即可在播放器界面上展示畫中畫按鈕。
自定義播放器并使用AVPictureInPictureController包裝
如果您使用自定義的播放器,并希望開啟畫中畫功能,可以使用AVPictureInPictureController對播放器進行包裝,簡單易用地實現(xiàn)畫中畫功能,并且AVPictureInPictureController內(nèi)部已經(jīng)實現(xiàn)了動畫效果。只需注意用戶需要自己實現(xiàn)畫中畫按鈕,系統(tǒng)已提供了相關(guān)API(pictureInPictureButtonStartImage)來使用畫中畫圖標。
懸浮窗
簡單理解為,可以利用UIWindow類型創(chuàng)建一個新的窗口,并將視頻播放器的視圖添加到該窗口上,通過手勢控制窗口的位置和大小,實現(xiàn)懸浮的畫中畫效果。
在iOS的設(shè)計準則中,明確規(guī)定了不允許在應(yīng)用程序中使用懸浮窗。使用懸浮窗可能會被蘋果拒絕審核或被迫下架。同時也要考慮到用戶體驗和隱私保護問題,因此在使用懸浮窗時需要謹慎考慮。
畫中畫
畫中畫(Picture-in-Picture)功能在iOS 9版本就已經(jīng)推出,但在之前的版本中,該功能只能在iPad上使用。直到iOS 14版本,iPhone用戶才能開始使用畫中畫功能。
畫中畫功能在iOS上有兩種實現(xiàn)方案:
支持iOS 14以上版本的老方案。
在iOS 14系統(tǒng)中,通過使用系統(tǒng)提供的AVPlayer來初始化AVPictureInPictureController, 進而實現(xiàn)在應(yīng)用程序壓后臺或進入二級頁面時出現(xiàn)畫中畫效果。這種方案適用于對播放器要求不太高的場景。
支持iOS 15版本以上的新方案。
在iOS 15及更高版本中,可以使用SamplebufferLayer來創(chuàng)建AVPictureInPictureController,以實現(xiàn)無縫的畫中畫播放。這種方案通常用于自定義播放器來實現(xiàn)畫中畫功能。目前,阿里云播放器SDK已提供畫中畫功能,操作詳情請參見畫中畫。
實現(xiàn)步驟如下:
iOS 15與iOS 14方案實現(xiàn)基本類似,本文以iOS 14老方案為例:
開啟后臺模式
導(dǎo)入框架#import <AVKit/AVKit.h>創(chuàng)建AVPictureInPictureController。
//1.判斷是否支持畫中畫功能 if ([AVPictureInPictureController isPictureInPictureSupported]) { //2.開啟權(quán)限 @try { NSError *error = nil; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionOrientationBack error:&error]; [[AVAudioSession sharedInstance] setActive:YES error:&error]; } @catch (NSException *exception) { NSLog(@"AVAudioSession發(fā)生錯誤"); } self.pipVC = [[AVPictureInPictureController alloc] initWithPlayerLayer:self.player]; self.pipVC.delegate = self; }
開啟或關(guān)閉畫中畫。
if (self.pipVC.isPictureInPictureActive) { [self.pipVC stopPictureInPicture]; } else { [self.pipVC startPictureInPicture]; }
代理AVPictureInPictureControllerDelegate。
// 即將開啟畫中畫 - (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController; // 已經(jīng)開啟畫中畫 - (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController; // 開啟畫中畫失敗 - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error; // 即將關(guān)閉畫中畫 - (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController; // 已經(jīng)關(guān)閉畫中畫 - (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController; // 關(guān)閉畫中畫且恢復(fù)播放界面 - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL restored))completionHandler;
重要通過一個全局變量持有畫中畫控制器,可以在pictureInPictureControllerWillStartPictureInPicture持有,在pictureInPictureControllerDidStopPictureInPicture釋放;
有時可能不是通過點擊畫中畫按鈕,而是通過其他途徑來打開當前的畫中畫控制器。可以在viewWillAppear方法中進行判斷并關(guān)閉。
在已經(jīng)存在畫中畫的情況下,若要開啟新的畫中畫,需等待完全關(guān)閉之后再進行新的開啟,以防止出現(xiàn)未知錯誤,因為關(guān)閉畫中畫是一個有過程的操作。
在創(chuàng)建AVPictureInPictureController并同時開啟畫中畫功能時,可能會出現(xiàn)失效的情況。如果遇到這種情況,建議延遲開啟畫中畫功能即可。