使用子母畫面 (PiP) 新增影片

試試 Compose 的方式
Jetpack Compose 是 Android 推薦的 UI 工具包。瞭解如何在 Compose 中支援子母畫面。

從 Android 8.0 (API 級別 26) 開始,Android 允許活動在子母畫面 (PiP) 模式下啟動。子母畫面是一種特殊的多視窗模式,主要用於影片播放。這個模式可讓使用者透過固定在畫面角落的小視窗觀看影片,同時繼續在主要畫面使用應用程式或瀏覽內容。

子母畫面會利用 Android 7.0 的多視窗 API 提供固定的影片重疊視窗。如要為應用程式新增子母畫面功能,您必須登錄支援子母畫面模式的活動、視需要將活動切換至子母畫面模式,並確認活動處於子母畫面模式時,UI 元素皆為隱藏狀態且影片會繼續播放。

子母畫面視窗會顯示在系統所選的畫面最頂層角落。

搭載 Android 14 (API 級別 34) 以上版本的 Android TV OS 相容裝置也支援 PiP。雖然兩者有很多相似之處,但在電視上使用 PiP 時,仍有其他需要考量的因素。

使用者如何與子母畫面視窗互動

使用者可以將子母畫面視窗拖曳到其他位置。從 Android 12 開始,使用者還可以執行以下操作:

  • 輕觸一下視窗來顯示全螢幕切換鈕、關閉按鈕、設定按鈕,以及應用程式提供的自訂動作 (例如播放控制項)。

  • 輕觸兩下視窗,即可在目前的子母畫面大小和最大的或最小的子母畫面大小之間切換。舉例來說,輕觸兩下已最大化的視窗,即可將視窗縮到最小,反之亦然。

  • 將視窗拖曳到左側或右側邊緣來隱藏視窗。如要取消隱藏視窗,請輕觸隱藏視窗的可見部分,或將視窗拖曳出來。

  • 使用雙指撥動方式進行縮放,調整子母畫面視窗的大小。

應用程式可控制目前活動進入子母畫面模式的時機,以下列舉幾個例子:

  • 如果使用者輕觸主畫面按鈕或向上滑動回到主畫面,活動就能進入子母畫面模式。Google 地圖之所以能在使用者執行其他活動的同時繼續顯示路線指引,就是基於這個機制。

  • 當使用者離開影片來瀏覽其他內容時,應用程式可將影片移至子母畫面模式。

  • 當使用者看完一集影片內容時,應用程式可將影片切換至子母畫面模式。主要畫面會顯示下一集系列影片的宣傳或摘要資訊。

  • 使用者觀看影片時,應用程式可讓使用者將其他內容排入佇列。影片會繼續在子母畫面模式下播放,主要畫面則顯示內容選取活動。

宣告子母畫面支援功能

根據預設,系統不會自動為應用程式提供子母畫面支援功能。如要讓應用程式支援子母畫面模式,請將 android:supportsPictureInPicture 設為 true,在資訊清單中登錄影片活動。此外,請指出活動會處理版面配置設定異動情形,這樣如果版面配置在子母畫面模式轉換期間有所變更,活動才不會重新啟動。

<activity android:name="VideoActivity"
    android:supportsPictureInPicture="true"
    android:configChanges=
        "screenSize|smallestScreenSize|screenLayout|orientation"
    ...

將活動切換至子母畫面模式

從 Android 12 開始,您可以將活動切換至子母畫面模式,方法是將 setAutoEnterEnabled 標記設為 true。有了這個設定,活動會視需要自動切換至子母畫面模式,而不需要在 onUserLeaveHint 中明確呼叫 enterPictureInPictureMode()。這項功能還有另一個好處,就是提供更流暢的轉場效果。詳情請參閱「透過手勢操作,讓轉換至子母畫面模式的過程更加順暢」。

如果指定 Android 11 以下版本,活動必須呼叫 enterPictureInPictureMode() 才能切換至子母畫面模式。舉例來說,以下程式碼會在使用者點選應用程式 UI 中的專屬按鈕時,將活動切換至子母畫面模式:

Kotlin

override fun onActionClicked(action: Action) {
    if (action.id.toInt() == R.id.lb_control_picture_in_picture) {
        activity?.enterPictureInPictureMode()
        return
    }
}

Java

@Override
public void onActionClicked(Action action) {
    if (action.getId() == R.id.lb_control_picture_in_picture) {
        getActivity().enterPictureInPictureMode();
        return;
    }
    ...
}

建議您加入相關邏輯,將活動切換至子母畫面模式,而非進入背景。舉例來說,如果使用者在應用程式進行導航期間按下主畫面按鈕或「最近」按鈕,Google 地圖就會切換至子母畫面模式。您可以覆寫 onUserLeaveHint() 來因應這種情況:

Kotlin

override fun onUserLeaveHint() {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode()
    }
}

Java

@Override
public void onUserLeaveHint () {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode();
    }
}

建議做法:為使用者提供優質的 PiP 轉換體驗

Android 12 大幅改善了全螢幕和子母畫面視窗之間的動畫轉場效果。我們強烈建議您導入所有適用的變更;完成後,這些變更會自動調整為折疊式裝置和平板電腦等大螢幕,無須額外採取任何行動。

如果您的應用程式不含適用的更新,PiP 轉場功能仍可正常運作,但動畫效果會比較粗糙。舉例來說,從全螢幕切換至 PiP 模式時,PiP 視窗會在轉換期間消失,然後在轉換完成後重新顯示。

這些異動包括:

  • 透過手勢操作,讓轉換至子母畫面模式的過程更加順暢
  • 設定適當的 sourceRectHint,用於進入及退出 PiP 模式
  • 停用非影片內容的流暢大小調整功能

如要啟用精緻的轉場體驗,請參考 Android Kotlin PictureInPicture 範例

透過手勢操作,讓轉換至子母畫面模式的過程更加順暢

從 Android 12 開始,setAutoEnterEnabled 標記可在使用手勢操作模式時,讓子母畫面模式中的影片內容轉換動畫更流暢,例如從全螢幕向上滑動至主畫面。

如要進行這項變更,請按照下列步驟操作,並參考這個範例

  1. 使用 setAutoEnterEnabled 建構 PictureInPictureParams.Builder

    Kotlin

    setPictureInPictureParams(PictureInPictureParams.Builder()
        .setAspectRatio(aspectRatio)
        .setSourceRectHint(sourceRectHint)
        .setAutoEnterEnabled(true)
        .build())
    

    Java

    setPictureInPictureParams(new PictureInPictureParams.Builder()
        .setAspectRatio(aspectRatio)
        .setSourceRectHint(sourceRectHint)
        .setAutoEnterEnabled(true)
        .build());
    
  2. 盡快使用最新的 PictureInPictureParams 呼叫 setPictureInPictureParams。應用程式不會等待 onUserLeaveHint 回呼 (就像在 Android 11 中一樣)。

    舉例來說,如果長寬比在後續播放時有所變動,您可能會想在第一次播放時呼叫 setPictureInPictureParams,並在後續播放時呼叫這個項目。

  3. 視需要呼叫 setAutoEnterEnabled(false)。舉例來說,如果目前處於暫停播放狀態,您可能不想進入子母畫面模式。

設定適當的 sourceRectHint,用於進入及退出子母畫面模式

從 Android 8.0 引入子母畫面功能開始,setSourceRectHint 會指出活動在轉換為子母畫面後可見的區域,例如影片播放器中的影片檢視區域邊界。

在 Android 12 中,系統會使用 sourceRectHint 在進入和結束子母畫面模式時,實作更流暢的動畫。

如要正確設定 sourceRectHint,以便進入及退出 PiP 模式,請按照下列步驟操作:

  1. 使用 sourceRectHint 的適當邊界建構 PictureInPictureParams。建議您也為影片播放器附加版面配置變更事件監聽器:

    Kotlin

    val mOnLayoutChangeListener =
    OnLayoutChangeListener { v: View?, oldLeft: Int,
            oldTop: Int, oldRight: Int, oldBottom: Int, newLeft: Int, newTop:
            Int, newRight: Int, newBottom: Int ->
        val sourceRectHint = Rect()
        mYourVideoView.getGlobalVisibleRect(sourceRectHint)
        val builder = PictureInPictureParams.Builder()
            .setSourceRectHint(sourceRectHint)
        setPictureInPictureParams(builder.build())
    }
    
    mYourVideoView.addOnLayoutChangeListener(mOnLayoutChangeListener)
    

    Java

    private final View.OnLayoutChangeListener mOnLayoutChangeListener =
            (v, oldLeft, oldTop, oldRight, oldBottom, newLeft, newTop, newRight,
            newBottom) -> {
        final Rect sourceRectHint = new Rect();
        mYourVideoView.getGlobalVisibleRect(sourceRectHint);
        final PictureInPictureParams.Builder builder =
            new PictureInPictureParams.Builder()
                .setSourceRectHint(sourceRectHint);
        setPictureInPictureParams(builder.build());
    };
    
    mYourVideoView.addOnLayoutChangeListener(mOnLayoutChangeListener);
    
  2. 視需要在系統啟動結束轉換前更新 sourceRectHint。當系統即將結束子母畫面模式時,活動的檢視區塊階層會呈現其目的地配置 (例如全螢幕)。應用程式可將版面配置變更事件監聽器附加至根檢視區塊或目標檢視區塊 (例如影片播放器檢視區塊),藉此偵測事件並在動畫開始前更新 sourceRectHint

    Kotlin

    // Listener is called immediately after the user exits PiP but before animating.
    playerView.addOnLayoutChangeListener { _, left, top, right, bottom,
                        oldLeft, oldTop, oldRight, oldBottom ->
        if (left != oldLeft
            || right != oldRight
            || top != oldTop
            || bottom != oldBottom) {
            // The playerView's bounds changed, update the source hint rect to
            // reflect its new bounds.
            val sourceRectHint = Rect()
            playerView.getGlobalVisibleRect(sourceRectHint)
            setPictureInPictureParams(
                PictureInPictureParams.Builder()
                    .setSourceRectHint(sourceRectHint)
                    .build()
            )
        }
    }
    
    

    Java

    // Listener is called right after the user exits PiP but before animating.
    playerView.addOnLayoutChangeListener((v, left, top, right, bottom,
                        oldLeft, oldTop, oldRight, oldBottom) -> {
        if (left != oldLeft
            || right != oldRight
            || top != oldTop
            || bottom != oldBottom) {
            // The playerView's bounds changed, update the source hint rect to
            // reflect its new bounds.
            final Rect sourceRectHint = new Rect();
            playerView.getGlobalVisibleRect(sourceRectHint);
            setPictureInPictureParams(
                new PictureInPictureParams.Builder()
                    .setSourceRectHint(sourceRectHint)
                    .build());
        }
    });
    
    

停用非影片內容的流暢大小調整功能

Android 12 新增了 setSeamlessResizeEnabled 標記,可在系統為子母畫面視窗中的非影片內容調整大小時,提供更加流暢的交錯淡出動畫效果。在過去,為子母畫面視窗中的非影片內容調整大小時,可能會產生假影並影響使用體驗。

如何停用非影片內容的流暢大小調整功能:

Kotlin

setPictureInPictureParams(PictureInPictureParams.Builder()
    .setSeamlessResizeEnabled(false)
    .build())

Java

setPictureInPictureParams(new PictureInPictureParams.Builder()
    .setSeamlessResizeEnabled(false)
    .build());

在子母畫面模式下處理 UI

當活動進入或結束子母畫面 (PiP) 模式時,系統會呼叫 Activity.onPictureInPictureModeChanged()Fragment.onPictureInPictureModeChanged()

Android 15 推出了變更,可確保進入子母畫面模式時的轉換更加流暢。這對應用程式有益,因為應用程式會在主要 UI 上重疊 UI 元素,並進入 PiP 模式。

開發人員會使用 onPictureInPictureModeChanged() 回呼來定義切換疊加 UI 元素顯示設定的邏輯。當 PiP 進入或退出動畫完成時,系統會觸發這個回呼。從 Android 15 開始,PictureInPictureUiState 類別包含新狀態。

有了這個新的 UI 狀態,以 Android 15 為目標的應用程式會在子母畫面動畫開始時,觀察 Activity#onPictureInPictureUiStateChanged() 回呼是否已使用 isTransitioningToPip() 叫用。在 PiP 模式下,許多 UI 元素都與應用程式無關,例如包含建議、即將播放的影片、評分和標題等資訊的檢視畫面或版面配置。當應用程式進入子母畫面模式時,請使用 onPictureInPictureUiStateChanged() 回呼來隱藏這些 UI 元素。當應用程式從子母畫面視窗進入全螢幕模式時,請使用 onPictureInPictureModeChanged() 回呼來取消隱藏這些元素,如以下範例所示:

Kotlin

override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
        if (pipState.isTransitioningToPip()) {
          // Hide UI elements.
        }
    }

Java

@Override
public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {
        if (pipState.isTransitioningToPip()) {
          // Hide UI elements.
        }
    }

Kotlin

override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
        if (isInPictureInPictureMode) {
          // Unhide UI elements.
        }
    }

Java

@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
        if (isInPictureInPictureMode) {
          // Unhide UI elements.
        }
    }

這個快速的 UI 元素可見度切換功能 (適用於 PiP 視窗),有助於確保 PiP 進入動畫更流暢且無閃爍。

覆寫這些回呼,以重新繪製活動的 UI 元素。請注意,在子母畫面模式下,活動會顯示在小型視窗中。此外,當應用程式處於子母畫面模式時,使用者無法與應用程式的 UI 元素互動,且可能難以查看小型 UI 元素的細節。為了提供最佳使用者體驗,請盡可能讓影片播放活動的 UI 保持精簡。

如果應用程式需要針對子母畫面模式提供自訂動作,請參閱本頁的「新增控制項」一節。在活動進入子母畫面模式前移除其他 UI 元素,並在活動再次進入全螢幕模式時將元素還原。

新增控制項

當使用者開啟視窗的選單 (在行動裝置上輕觸視窗,或使用電視遙控器選取選單) 時,子母畫面視窗可顯示控制項。

如果應用程式有進行中的媒體工作階段,就會顯示「播放」、「暫停」、「下一個」和「上一個」控制項。

您也可以在進入子母畫面模式前使用 PictureInPictureParams.Builder.setActions() 建構 PictureInPictureParams,藉此明確指定自訂動作,並在使用 enterPictureInPictureMode(android.app.PictureInPictureParams)setPictureInPictureParams(android.app.PictureInPictureParams) 進入子母畫面模式時傳遞參數。小心一點,如果您嘗試新增的動作數量超過 getMaxNumPictureInPictureActions(),則只會取得上限值。

在子母畫面模式下繼續播放影片

當活動切換至子母畫面模式時,系統會將活動設為暫停狀態,並呼叫活動的 onPause() 方法。如果活動在轉換至子母畫面模式時處於暫停狀態,影片不應暫停,而要繼續播放。

在 Android 7.0 以上版本中,當系統呼叫活動的 onStop()onStart() 時,您應該要暫停並繼續播放影片。這樣您就不必檢查應用程式在 onPause() 中是否處於子母畫面模式,也不必明確繼續播放。

如果您尚未將 setAutoEnterEnabled 標記設為 true,且需要在 onPause() 實作中暫停播放,請呼叫 isInPictureInPictureMode() 來檢查子母畫面模式,並妥善處理播放作業。例如:

Kotlin

override fun onPause() {
    super.onPause()
    // If called while in PiP mode, do not pause playback.
    if (isInPictureInPictureMode) {
        // Continue playback.
    } else {
        // Use existing playback logic for paused activity behavior.
    }
}

Java

@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.
        ...
    }
}

當活動從子母畫面模式切換回全螢幕模式時,系統會讓活動繼續進行並呼叫 onResume() 方法。

針對子母畫面使用單一播放活動

在應用程式中,有可能使用者在主要畫面瀏覽內容時選取新影片,而同時也有影片播放活動在子母畫面模式下進行。在這種情況下,請以全螢幕模式在現有播放活動中播放新影片,而不要啟動新活動,避免造成使用者混淆。

為了確保只有一個活動會用於影片播放要求,且該活動會視需要進入或結束子母畫面模式,請在資訊清單中將活動的 android:launchMode 設為 singleTask

<activity android:name="VideoActivity"
    ...
    android:supportsPictureInPicture="true"
    android:launchMode="singleTask"
    ...

在活動中覆寫 onNewIntent() 並處理新影片,視需要停止任何現有的影片播放作業。

最佳做法

在 RAM 較少的裝置上,系統可能會停用子母畫面模式。在應用程式使用子母畫面模式前,請先呼叫 hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE),確認是否能使用該功能。

子母畫面功能適用於播放全螢幕影片的活動。將活動切換至子母畫面模式時,請不要顯示影片以外的內容。追蹤活動進入子母畫面模式的時機,並依照「在子母畫面模式下處理 UI」一節的說明隱藏 UI 元素。

根據預設,活動處於子母畫面模式時不會取得輸入焦點。如要在子母畫面模式下接收輸入事件,請使用 MediaSession.setCallback()。如要進一步瞭解如何使用 setCallback(),請參閱「顯示『現正播放』資訊卡」。

當應用程式處於子母畫面模式時,子母畫面視窗中的影片播放作業可能會對其他應用程式 (例如音樂播放器應用程式或語音搜尋應用程式) 造成音訊干擾。為避免這種情況發生,請在開始播放影片時要求音訊焦點,並處理音訊焦點變更通知,如「管理音訊焦點」一文所述。在子母畫面模式下,如果系統顯示通知,指出您已失去音訊焦點,請暫停或停止播放影片。

請注意,當應用程式要進入子母畫面模式時,只有上層活動會進入該模式。但現在於某些情況下 (例如在多視窗裝置上),下層活動也可能會顯示,並會隨著子母畫面模式再次出現。請根據實際情形處理這種狀況,包括下層活動取得 onResume()onPause() 回呼的情況。使用者也可能會與活動互動。舉例來說,假設您顯示了影片清單活動,而影片播放活動處於子母畫面模式。在這種情況下,使用者可能會從清單中選取新影片,這時子母畫面活動應該要根據使用者的互動有所更新。

其他程式碼範例

如要下載以 Kotlin 編寫的範例應用程式,請參閱「Android PictureInPicture 範例 (Kotlin)」。