使用子母畫面 (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 時,仍有其他需要考量的因素。

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

使用者可以將子母畫面視窗拖曳到其他位置。從 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 中的專屬按鈕時,將活動切換至 PiP 模式:

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();
    }
}

建議做法:為使用者提供優質的子母畫面轉換體驗

Android 12 大幅改善了全螢幕和子母畫面視窗之間的動畫轉場效果。強烈建議您實作所有適用的變更;完成後,相關變更就會自動縮放至大螢幕 (例如折疊式裝置和平板電腦),您不必執行其他操作。

如果您的應用程式沒有提供適用的更新,子母畫面轉換仍可正常運作,但動畫效果較差。舉例來說,如果從全螢幕轉換至子母畫面模式,可能會導致子母畫面視窗在轉換期間消失,等到轉換完畢後再次出現。

這些異動包括:

  • 透過手勢操作讓切換子母畫面模式更順暢
  • 設定適當的 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 上方 (進入子母畫面模式),這項功能就能派上用場。

開發人員會使用 onPictureInPictureModeChanged() 回呼定義邏輯,藉此切換疊加 UI 元素的顯示設定。子母畫面進入或離開動畫播放完畢時,就會觸發此回呼。 從 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)」。