«Картинка в картинке» (PiP) — это особый тип многооконного режима, который чаще всего используется для воспроизведения видео. Он позволяет пользователю смотреть видео в небольшом окне, закрепленном в углу экрана, одновременно перемещаясь между приложениями или просматривая контент на главном экране.
PiP использует многооконные API, доступные в Android 7.0, для обеспечения прикрепленного окна наложения видео. Чтобы добавить PiP в свое приложение, вам необходимо зарегистрировать свое действие, при необходимости переключить его в режим PiP и убедиться, что элементы пользовательского интерфейса скрыты и воспроизведение видео продолжается, когда действие находится в режиме PiP.
В этом руководстве описывается, как добавить PiP в Compose в ваше приложение с помощью реализации Compose видео. Посетите приложение Socialite , чтобы увидеть эти лучшие практики в действии.
Настройте свое приложение для PiP
В теге активности файла AndroidManifest.xml
выполните следующие действия:
- Добавьте
supportsPictureInPicture
и установите для него значениеtrue
, чтобы заявить, что вы будете использовать PiP в своем приложении. Добавьте
configChanges
и установите для него значениеorientation|screenLayout|screenSize|smallestScreenSize
чтобы указать, что ваша активность обрабатывает изменения конфигурации макета. Таким образом, ваша деятельность не перезапускается при изменении макета во время перехода в режим PiP.<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") }
Добавьте приложение «PiP при выходе» для версий до Android 12.
Чтобы добавить PiP для версий до Android 12, используйте addOnUserLeaveHintProvider
. Выполните следующие действия, чтобы добавить PiP для версий до Android 12:
- Добавьте шлюз версии, чтобы доступ к этому коду был возможен только в версиях от O до R.
- Используйте
DisposableEffect
сContext
в качестве ключа. - Внутри
DisposableEffect
определите поведение, когдаonUserLeaveHintProvider
запускается с помощью лямбда-выражения. В лямбде вызовитеenterPictureInPictureMode()
вfindActivity()
и передайтеPictureInPictureParams.Builder().build()
. - Добавьте
addOnUserLeaveHintListener
с помощьюfindActivity()
и передайте лямбду. - В
onDispose
добавьтеremoveOnUserLeaveHintListener
с помощьюfindActivity()
и передайте лямбда-выражение.
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") }
Добавьте приложение PiP при выходе из Android 12 после версии.
В версии после Android 12 PictureInPictureParams.Builder
добавляется через модификатор, который передается видеопроигрывателю приложения.
- Создайте
modifier
и вызовите для негоonGloballyPositioned
. Координаты макета будут использоваться на более позднем этапе. - Создайте переменную для
PictureInPictureParams.Builder()
. - Добавьте оператор
if
чтобы проверить, является ли SDK версией S или выше. Если да, добавьтеsetAutoEnterEnabled
в конструктор и установите для него значениеtrue
, чтобы переходить в режим PiP при пролистывании. Это обеспечивает более плавную анимацию, чем при использованииenterPictureInPictureMode
. - Используйте
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 с помощью кнопки
Чтобы войти в режим PiP нажатием кнопки, вызовите enterPictureInPictureMode()
в findActivity()
.
Параметры уже установлены предыдущими вызовами 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!") }
Управляйте своим пользовательским интерфейсом в режиме PiP
Когда вы входите в режим «картинка в картинке», весь пользовательский интерфейс вашего приложения переходит в окно «картинка в картинке», если вы не укажете, как ваш пользовательский интерфейс должен выглядеть в режиме «картинка в картинке» и из него.
Во-первых, вам нужно знать, находится ли ваше приложение в режиме PiP или нет. Для этого вы можете использовать OnPictureInPictureModeChangedProvider
. Код ниже сообщает вам, находится ли ваше приложение в режиме PiP.
@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()
чтобы переключать элементы пользовательского интерфейса, которые будут отображаться, когда приложение переходит в режим PiP:
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() }
Убедитесь, что ваше приложение переходит в режим PiP в нужное время.
Ваше приложение не должно переходить в режим PiP в следующих ситуациях:
- Если видео остановлено или поставлено на паузу.
- Если вы находитесь на другой странице приложения, чем видеоплеер.
Чтобы контролировать, когда ваше приложение переходит в режим PiP, добавьте переменную, которая отслеживает состояние видеоплеера, используя mutableStateOf
.
Переключение состояния в зависимости от того, воспроизводится ли видео
Чтобы переключить состояние в зависимости от того, воспроизводится ли видеопроигрыватель, добавьте прослушиватель в видеопроигрыватель. Переключите состояние вашей переменной состояния в зависимости от того, играет игрок или нет:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
Переключение состояния в зависимости от того, выпущен ли игрок
Когда проигрыватель отпущен, установите для переменной состояния значение false
:
fun releasePlayer() { shouldEnterPipMode = false }
Используйте состояние, чтобы определить, включен ли режим PiP (до Android 12)
- Поскольку при добавлении PiP до 12 используется
DisposableEffect
, вам необходимо создать новую переменную с помощьюrememberUpdatedState
сnewValue
, установленным в качестве переменной состояния. Это гарантирует, что обновленная версия будет использоваться вDisposableEffect
. В лямбда-выражение, определяющее поведение при срабатывании
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") }
Используйте состояние, чтобы определить, включен ли режим 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
для реализации плавной анимации.
API setSourceRectHint
создает более плавную анимацию для перехода в режим PiP. В Android 12+ он также создает более плавную анимацию выхода из режима PiP. Добавьте этот API в конструктор PiP, чтобы указать область действия, которая будет видна после перехода в PiP.
- Добавляйте
setSourceRectHint()
вbuilder
только в том случае, если состояние определяет, что приложение должно перейти в режим PiP. Это позволяет избежать вычисленияsourceRect
, когда приложению не требуется вводить PiP. - Чтобы установить значение
sourceRect
, используйтеlayoutCoordinates
, заданные функциейonGloballyPositioned
в модификаторе. - Вызовите
setSourceRectHint()
вbuilder
и передайте переменную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
, чтобы установить соотношение сторон окна PiP.
Чтобы установить соотношение сторон окна PiP, вы можете выбрать определенное соотношение сторон или использовать ширину и высоту размера видео плеера. Если вы используете проигрыватель 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.
Добавить удаленные действия
Если вы хотите добавить элементы управления (воспроизведение, пауза и т. д.) в окно PiP, создайте 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
для элементов управления в окне PiP. - Затем добавьте
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)
Следующие шаги
В этом руководстве вы узнали о лучших практиках добавления PiP в Compose как до Android 12, так и после Android 12.
- Посетите приложение Socialite , чтобы увидеть лучшие практики Compose PiP в действии.
- Дополнительную информацию см. в руководстве по проектированию PiP .