Добавьте картинку в картинке (PiP) в свое приложение с помощью видеоплеера Compose.

«Картинка в картинке» (PiP) — это особый тип многооконного режима, который чаще всего используется для воспроизведения видео. Он позволяет пользователю смотреть видео в небольшом окне, закрепленном в углу экрана, одновременно перемещаясь между приложениями или просматривая контент на главном экране.

PiP использует многооконные API, доступные в Android 7.0, для обеспечения прикрепленного окна наложения видео. Чтобы добавить PiP в свое приложение, вам необходимо зарегистрировать свое действие, при необходимости переключить его в режим PiP и убедиться, что элементы пользовательского интерфейса скрыты и воспроизведение видео продолжается, когда действие находится в режиме PiP.

В этом руководстве описывается, как добавить PiP в Compose в ваше приложение с помощью реализации Compose видео. Посетите приложение Socialite , чтобы увидеть эти лучшие практики в действии.

Настройте свое приложение для PiP

В теге активности файла AndroidManifest.xml выполните следующие действия:

  1. Добавьте supportsPictureInPicture и установите для него значение true , чтобы заявить, что вы будете использовать PiP в своем приложении.
  2. Добавьте 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 выполните следующие действия:

  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")
    }

Добавьте приложение «PiP при выходе» для версий до Android 12.

Чтобы добавить PiP для версий до Android 12, используйте addOnUserLeaveHintProvider . Выполните следующие действия, чтобы добавить PiP для версий до Android 12:

  1. Добавьте шлюз версии, чтобы доступ к этому коду был возможен только в версиях от O до R.
  2. Используйте DisposableEffect с Context в качестве ключа.
  3. Внутри DisposableEffect определите поведение, когда onUserLeaveHintProvider запускается с помощью лямбда-выражения. В лямбде вызовите enterPictureInPictureMode() в findActivity() и передайте PictureInPictureParams.Builder().build() .
  4. Добавьте addOnUserLeaveHintListener с помощью findActivity() и передайте лямбду.
  5. В 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 добавляется через модификатор, который передается видеопроигрывателю приложения.

  1. Создайте modifier и вызовите для него onGloballyPositioned . Координаты макета будут использоваться на более позднем этапе.
  2. Создайте переменную для PictureInPictureParams.Builder() .
  3. Добавьте оператор if чтобы проверить, является ли SDK версией S или выше. Если да, добавьте setAutoEnterEnabled в конструктор и установите для него значение true , чтобы переходить в режим PiP при пролистывании. Это обеспечивает более плавную анимацию, чем при использовании 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 с помощью кнопки

Чтобы войти в режим 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)

  1. Поскольку при добавлении PiP до 12 используется DisposableEffect , вам необходимо создать новую переменную с помощью rememberUpdatedState с newValue , установленным в качестве переменной состояния. Это гарантирует, что обновленная версия будет использоваться в DisposableEffect .
  2. В лямбда-выражение, определяющее поведение при срабатывании 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.

  1. Добавляйте setSourceRectHint() в builder только в том случае, если состояние определяет, что приложение должно перейти в режим PiP. Это позволяет избежать вычисления sourceRect , когда приложению не требуется вводить PiP.
  2. Чтобы установить значение sourceRect , используйте layoutCoordinates , заданные функцией onGloballyPositioned в модификаторе.
  3. Вызовите 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 для каждого элемента управления, который вы хотите добавить.

  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 для элементов управления в окне PiP.
  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)

Следующие шаги

В этом руководстве вы узнали о лучших практиках добавления PiP в Compose как до Android 12, так и после Android 12.