使用 Compose 影片播放器將子母畫面 (PiP) 新增至應用程式

子母畫面 (PiP) 是一種特殊的多視窗模式,最常用於 影片播放。能讓使用者透過固定顯示的小視窗觀看影片 這個畫面角落的切換鈕 主畫面。

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

本指南說明如何使用 Compose 影片,在 Compose 中將子母畫面新增至應用程式 。瞭解如何使用 Socialite 應用程式,看看這些相片中的最佳解答 實際做法。

為應用程式設定子母畫面模式

AndroidManifest.xml 檔案的活動標記中執行以下操作:

  1. 新增 supportsPictureInPicture 並將其設為 true,宣告您將在應用程式中使用 PiP。
  2. 新增 configChanges 並設為 orientation|screenLayout|screenSize|smallestScreenSize 可指定 活動會處理版面配置設定變更。這樣一來,如果版面配置在子母畫面模式轉換期間有所變更,活動就不會重新啟動。

      <activity
        android:name=".SnippetsActivity"
        android:exported="true"
        android:supportsPictureInPicture="true"
        android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
        android:theme="@style/Theme.Snippets">
    敬上

在 Compose 程式碼中執行以下操作:

  1. Context 中加入這個額外資訊。您將多次使用這個額外資訊 完整存取活動
    internal fun Context.findActivity(): ComponentActivity {
        var context = this
        while (context is ContextWrapper) {
            if (context is ComponentActivity) return context
            context = context.baseContext
        }
        throw IllegalStateException("Picture in picture should be called in the context of an Activity")
    }

為 Android 12 以下版本的假應用程式新增子母畫面

如要在 Android 12 以下版本中新增子母畫面,請使用 addOnUserLeaveHintProvider。追蹤 請按照下列步驟新增 Android 12 以下版本的子母畫面:

  1. 新增版本閘門,讓只能在版本 O 前存取這個程式碼。
  2. 使用具有 ContextDisposableEffect 做為索引鍵。
  3. DisposableEffect 中,定義當 onUserLeaveHintProvider 是使用 lambda 觸發。在 lambda 中,呼叫 findActivity() 上的 enterPictureInPictureMode() 並傳入 PictureInPictureParams.Builder().build()
  4. 使用 findActivity() 新增 addOnUserLeaveHintListener,並傳入 lambda。
  5. onDispose 中,使用 findActivity() 新增 removeOnUserLeaveHintListener 並傳入 lambda。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
    Build.VERSION.SDK_INT < Build.VERSION_CODES.S
) {
    val context = LocalContext.current
    DisposableEffect(context) {
        val onUserLeaveBehavior: () -> Unit = {
            context.findActivity()
                .enterPictureInPictureMode(PictureInPictureParams.Builder().build())
        }
        context.findActivity().addOnUserLeaveHintListener(
            onUserLeaveBehavior
        )
        onDispose {
            context.findActivity().removeOnUserLeaveHintListener(
                onUserLeaveBehavior
            )
        }
    }
} else {
    Log.i("PiP info", "API does not support PiP")
}

為 Android 12 後的版本新增子母畫面功能

在 Android 12 以後,PictureInPictureParams.Builder是以 傳遞至應用程式的影片播放器。

  1. 建立 modifier,並對其呼叫 onGloballyPositioned。版面配置 稍後的步驟會用到。
  2. PictureInPictureParams.Builder() 建立變數。
  3. 新增 if 陳述式,檢查 SDK 是否為 S 以上版本。如果有,請新增 將 setAutoEnterEnabled 設為建構工具,並將其設為 true 即可進入子母畫面模式 模式。這比透過 enterPictureInPictureMode 提供更流暢的動畫。
  4. 使用 findActivity() 呼叫 setPictureInPictureParams()。撥號至下列時段的build()builder 並傳入。

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(true)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}
VideoPlayer(pipModifier)

透過按鈕新增子母畫面

如要透過按一下按鈕進入 PiP 模式,請在 findActivity() 上呼叫 enterPictureInPictureMode()

參數已由之前對 PictureInPictureParams.Builder,因此您不需要設定新參數 。不過,如果您想變更按鈕點擊的任何參數,可以在此設定。

val context = LocalContext.current
Button(onClick = {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        context.findActivity().enterPictureInPictureMode(
            PictureInPictureParams.Builder().build()
        )
    } else {
        Log.i(PIP_TAG, "API does not support PiP")
    }
}) {
    Text(text = "Enter PiP mode!")
}

在子母畫面模式下處理使用者介面

進入子母畫面模式後,應用程式的整個 UI 都會進入子母畫面視窗,除非您 指定 UI 在子母畫面模式下的外觀。

首先,您需要知道應用程式是否處於子母畫面模式。您可以使用 OnPictureInPictureModeChangedProvider 達成此目標。下列程式碼會指出應用程式是否處於子母畫面模式。

@Composable
fun rememberIsInPipMode(): Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val activity = LocalContext.current.findActivity()
        var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) }
        DisposableEffect(activity) {
            val observer = Consumer<PictureInPictureModeChangedInfo> { info ->
                pipMode = info.isInPictureInPictureMode
            }
            activity.addOnPictureInPictureModeChangedListener(
                observer
            )
            onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) }
        }
        return pipMode
    } else {
        return false
    }
}

現在,您可以使用 rememberIsInPipMode() 切換要顯示的 UI 元素 應用程式進入子母畫面模式時:

val inPipMode = rememberIsInPipMode()

Column(modifier = modifier) {
    // This text will only show up when the app is not in PiP mode
    if (!inPipMode) {
        Text(
            text = "Picture in Picture",
        )
    }
    VideoPlayer()
}

確認應用程式會在適當的時機進入子母畫面模式

在下列情況下,應用程式不應進入子母畫面模式:

  • 影片停止或暫停時。
  • 如果您位於與影片播放器不同的應用程式頁面。

如要控制應用程式進入子母畫面模式的時機,請新增追蹤狀態的變數 並透過 mutableStateOf 載入影片播放器的畫面。

根據影片播放狀態切換狀態

如要根據影片播放器是否正在播放來切換狀態,請在影片播放器上新增事件監聽器。根據玩家是否啟用狀態變數 是否正在播放:

player.addListener(object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
        shouldEnterPipMode = isPlaying
    }
})

根據播放器是否釋出來切換狀態

當玩家釋放時,請將狀態變數設為 false

fun releasePlayer() {
    shouldEnterPipMode = false
}

使用狀態來定義是否已進入子母畫面模式 (Android 12 之前的版本)

  1. 在新增子母畫面 12 之前的版本時,系統會使用 DisposableEffect,因此您需要建立 rememberUpdatedState 建立一個新變數,並將 newValue 設為您的 狀態變數。以確保系統會在 DisposableEffect
  2. 在 lambda 中定義 OnUserLeaveHintListener 時的行為 觸發後,請新增 if 陳述式,在呼叫 enterPictureInPictureMode():

    val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
        Build.VERSION.SDK_INT < Build.VERSION_CODES.S
    ) {
        val context = LocalContext.current
        DisposableEffect(context) {
            val onUserLeaveBehavior: () -> Unit = {
                if (currentShouldEnterPipMode) {
                    context.findActivity()
                        .enterPictureInPictureMode(PictureInPictureParams.Builder().build())
                }
            }
            context.findActivity().addOnUserLeaveHintListener(
                onUserLeaveBehavior
            )
            onDispose {
                context.findActivity().removeOnUserLeaveHintListener(
                    onUserLeaveBehavior
                )
            }
        }
    } else {
        Log.i("PiP info", "API does not support PiP")
    }

使用狀態來定義是否已進入子母畫面模式 (Android 12 後)

將狀態變數傳遞至 setAutoEnterEnabled,讓應用程式僅進入 各種時間點的子母畫面模式:

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()

    // Add autoEnterEnabled for versions S and up
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

使用 setSourceRectHint 實作流暢動畫

setSourceRectHint API 可建立更流暢的動畫,讓應用程式進入 PiP 模式。在 Android 12 以上版本中,這項功能也能讓結束子母畫面模式的動畫更流暢。 將這個 API 新增至子母畫面建構工具,代表用於存放活動的區域 也能看到

  1. 只有在狀態定義了 setSourceRectHint()builder 應用程式應進入子母畫面模式。這可避免在應用程式發生時計算 sourceRect 就不需要進入子母畫面模式。
  2. 如要設定 sourceRect 值,請使用系統提供的 layoutCoordinates 使用來自修飾符的 onGloballyPositioned 函式
  3. builder 呼叫 setSourceRectHint(),並傳入 sourceRect 變數。

val context = LocalContext.current

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()
    if (shouldEnterPipMode) {
        val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
        builder.setSourceRectHint(sourceRect)
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

使用 setAspectRatio 設定子母畫面視窗的顯示比例

如要設定子母畫面視窗的長寬比,您可以選擇特定的 或使用播放器影片大小的寬度和高度。如果您是 使用 media3 播放器,請確認播放器並非空值, 設定長寬比前,影片大小不等於 VideoSize.UNKNOWN 比例。

val context = LocalContext.current

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()
    if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) {
        val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
        builder.setSourceRectHint(sourceRect)
        builder.setAspectRatio(
            Rational(player.videoSize.width, player.videoSize.height)
        )
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

如果您使用自訂播放器,請設定播放器高度的顯示比例 以及寬度和寬度請注意 在初始化期間調整大小,前提是超出預期程式碼的有效邊界 如果顯示比例可能有誤 您的應用程式就會當機你可能需要新增檢查 和媒體的顯示比例類似 廣告。

新增遠端動作

如要在子母畫面視窗新增控制項 (播放、暫停等),請建立 RemoteAction

  1. 為直播控制項加入常數:
    // Constant for broadcast receiver
    const val ACTION_BROADCAST_CONTROL = "broadcast_control"
    
    // Intent extras for broadcast controls from Picture-in-Picture mode.
    const val EXTRA_CONTROL_TYPE = "control_type"
    const val EXTRA_CONTROL_PLAY = 1
    const val EXTRA_CONTROL_PAUSE = 2
  2. 為子母畫面視窗中的控制項建立 RemoteActions 清單。
  3. 接著,新增 BroadcastReceiver 並覆寫 onReceive() 來設定 以及各按鈕的動作使用 DisposableEffect 註冊 接收方和遠端動作棄置玩家後,請取消註冊 接收器。
    @RequiresApi(Build.VERSION_CODES.O)
    @Composable
    fun PlayerBroadcastReceiver(player: Player?) {
        val isInPipMode = rememberIsInPipMode()
        if (!isInPipMode || player == null) {
            // Broadcast receiver is only used if app is in PiP mode and player is non null
            return
        }
        val context = LocalContext.current
    
        DisposableEffect(player) {
            val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
                override fun onReceive(context: Context?, intent: Intent?) {
                    if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) {
                        return
                    }
    
                    when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
                        EXTRA_CONTROL_PAUSE -> player.pause()
                        EXTRA_CONTROL_PLAY -> player.play()
                    }
                }
            }
            ContextCompat.registerReceiver(
                context,
                broadcastReceiver,
                IntentFilter(ACTION_BROADCAST_CONTROL),
                ContextCompat.RECEIVER_NOT_EXPORTED
            )
            onDispose {
                context.unregisterReceiver(broadcastReceiver)
            }
        }
    }
  4. 將您的遠端動作清單傳遞至 PictureInPictureParams.Builder:
    val context = LocalContext.current
    
    val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
        val builder = PictureInPictureParams.Builder()
        builder.setActions(
            listOfRemoteActions()
        )
    
        if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) {
            val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
            builder.setSourceRectHint(sourceRect)
            builder.setAspectRatio(
                Rational(player.videoSize.width, player.videoSize.height)
            )
        }
    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            builder.setAutoEnterEnabled(shouldEnterPipMode)
        }
        context.findActivity().setPictureInPictureParams(builder.build())
    }
    VideoPlayer(modifier = pipModifier)

後續步驟

在本指南中,您瞭解了在 Compose 中新增 PiP 的最佳做法,無論是 Android 12 之前或之後皆適用。

  • 請參閱 Socialite 應用程式,以瞭解 編寫子母畫面的實際運作情形。
  • 詳情請參閱 PiP 設計指南