子母畫面 (PiP) 是一種特殊的多視窗模式,主要用於影片播放。可讓使用者透過固定在螢幕角落的小視窗觀看影片,同時在應用程式內瀏覽或瀏覽主畫面內容。
子母畫面會利用 Android 7.0 提供的多視窗 API 提供固定的影片重疊視窗。如要在應用程式中加入子母畫面功能,您必須註冊活動、視需要將活動切換至子母畫面模式,並確認在活動處於子母畫面模式時,UI 元素為隱藏狀態,且影片能繼續播放。
本指南說明如何使用 Compose 影片實作項目,在 Compose 中新增子母畫面。如要瞭解這些最佳做法的實際運作情形,請參閱 Socialite 應用程式。
設定應用程式以使用子母畫面模式
在 AndroidManifest.xml
檔案的活動標記中,執行以下操作:
- 新增
supportsPictureInPicture
並將其設為true
,宣告您將在應用程式中使用子母畫面。 新增
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 程式碼中執行下列操作:
- 前往
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 以下版本的子母畫面:
- 新增版本閘道,讓這個程式碼只能在 O 到 R 版本中存取。
- 請使用
DisposableEffect
搭配Context
做為索引鍵。 - 在
DisposableEffect
中,定義使用 lambda 觸發onUserLeaveHintProvider
時的行為。在 lambda 中,對findActivity()
呼叫enterPictureInPictureMode()
,並傳入PictureInPictureParams.Builder().build()
。 - 使用
findActivity()
新增addOnUserLeaveHintListener
,並傳入 lambda。 - 在
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_TAG, "API does not support PiP") }
在 Android 12 以後的離開應用程式中新增子母畫面模式
在 Android 12 後,請透過傳遞至應用程式影片播放器的修飾符來新增 PictureInPictureParams.Builder
。
- 建立
modifier
並對其呼叫onGloballyPositioned
。您將在後續步驟中使用版面配置座標。 - 為
PictureInPictureParams.Builder()
建立變數。 - 新增
if
陳述式,檢查 SDK 是否為 S 以上。如果是,請將setAutoEnterEnabled
新增至建構工具並將其設為true
,以便在滑動時進入子母畫面模式。相較於處理enterPictureInPictureMode
,動畫效果更流暢。 - 使用
findActivity()
呼叫setPictureInPictureParams()
。對builder
呼叫build()
並傳入。
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)
透過按鈕新增子母畫面
如要透過點選按鈕進入子母畫面模式,請在 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 就會進入子母畫面視窗,除非您指定 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 以下版本)
- 由於新增子母畫面 12 前的版本會使用
DisposableEffect
,因此您必須透過rememberUpdatedState
建立新變數,並將newValue
設為狀態變數。這可確保DisposableEffect
使用更新版本。 在定義
OnUserLeaveHintListener
觸發時行為的 lambda 中,新增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_TAG, "API does not support PiP") }
使用狀態定義是否進入子母畫面模式 (Android 12 後)
將狀態變數傳遞至 setAutoEnterEnabled
,讓應用程式只在適當時機進入 PiP 模式:
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 可讓進入子母畫面模式時提供更流暢的動畫。在 Android 12 以上版本中,還可以為退出子母畫面模式建立更流暢的動畫。將這個 API 新增至子母畫面建構工具,表示在轉換至子母畫面後顯示的活動區域。
- 只有在狀態定義應用程式應進入子母畫面模式時,才能將
setSourceRectHint()
新增至builder
。這可避免在應用程式不需要進入子母畫面的情況下計算sourceRect
。 - 如要設定
sourceRect
值,請使用該修飾符的onGloballyPositioned
函式所提供的layoutCoordinates
。 - 在
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)
如果使用的是自訂播放器,請使用播放器專屬的語法,設定玩家高度和寬度的長寬比。請注意,如果玩家在初始化期間調整大小,只要未達顯示比例的有效邊界,應用程式就會當機。您可能需要在計算顯示比例時新增檢查,就像使用 Media3 播放器的運作方式一樣。
新增遠端動作
如果想為子母畫面視窗新增控制項 (播放、暫停等),請為每個要新增的控制項建立 RemoteAction
。
- 為廣播控制項新增常數:
// 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
- 為子母畫面視窗的控制項建立
RemoteActions
清單。 - 接著,新增
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) } } }
- 將遠端動作清單傳入
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)
後續步驟
在這個指南中,您學到了在 Android 12 以下版本和 Android 12 之前的 Compose 中新增子母畫面的最佳做法。