Как использовать библиотеку Jetpack Picture-in-Picture

Библиотека Jetpack для режима "Картинка в картинке" – это простое и надежное решение, которое позволяет разработчикам приложений Android реализовать режим "Картинка в картинке", особенно в приложениях для воспроизведения медиаконтента, видеосвязи и навигации. Благодаря единому API библиотека помогает избавиться от шаблонного кода и распространенных ошибок в приложении, а также повысить качество работы режима "картинка в картинке".

Библиотека Jetpack для режима "картинка в картинке" упрощает работу с существующими API, устраняя ряд ключевых проблем и несоответствий в экосистеме Android:

  • Фрагментация ОС. Библиотека автоматически обрабатывает различия в вызовах API для режима "Картинка в картинке" в разных версиях Android. Например, до Android 12 используется enterPictureInPictureMode, а после – isAutoEnterEnabled. Разработчикам не нужно учитывать различия между версиями.
  • Неправильные параметры режима "Картинка в картинке". API предоставляет единое решение для правильной настройки параметров режима "Картинка в картинке", например setSourceRectHint, чтобы создавать плавные и качественные анимации во время воспроизведения медиаконтента.
  • Унифицированные обратные вызовы состояния картинки в картинке. onPictureInPictureModeChanged и onPictureInPictureUiStateChanged объединены в единый интерфейс обратного вызова (PictureInPictureDelegate.OnPictureInPictureEventListener), что упрощает управление состоянием и интерфейсом.
  • Меньше шаблонного кода. Библиотека позволяет сократить количество повторяющегося шаблонного кода, предлагая готовые наборы RemoteActions для распространенных сценариев, таких как управление воспроизведением и действия во время видеовызовов.
  • Перспективы. Дополнительные функции режима "Картинка в картинке" предоставляются через библиотеку Jetpack, что позволяет разработчикам получать доступ к дополнительным возможностям с минимальными усилиями или без них.

Процесс переноса

Определите категорию сценария использования приложения и устаревшую логику режима "Картинка в картинке":

Категории: "Воспроизведение видео", "Навигация" или "Видеовызов".

Устаревшая логика определения режима "картинка в картинке":

  • onUserLeaveHint
  • setAutoEnterEnabled
  • onPictureInPictureModeChanged
  • onPictureInPictureUiStateChanged
  • setPictureInPictureParams.

2. Конфигурация AndroidManifest

Убедитесь, что в файле AndroidManifest.xml для Activity, которое переходит в режим "Картинка в картинке", указана поддержка необходимых изменений конфигурации (configChanges). Это позволит избежать ненужных перезапусков.

<activity
android:name="VideoActivity" android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
</activity>

3. Настройка среды

Добавьте необходимые зависимости в файл build.gradle:

dependencies {
implementation("androidx.core:core:1.18.0")
implementation("androidx.activity:activity:1.13.0")
implementation("androidx.core:core-pip:1.0.0-alpha02") }

Используйте последние версии библиотек AndroidX для зависимостей. Информацию о них можно найти на странице Выпуски.

4. Выбор и инициализация шаблона

Выберите шаблон реализации, который лучше всего подходит для вашего приложения:

  • Навигация и видеовызовы. BasicPictureInPicture. Плавное изменение размера обычно не поддерживается, и подсказка sourceRectHint не требуется.
  • Воспроизведение видео. VideoPlaybackPictureInPicture автоматически отслеживает границы проигрывателя для подсказки исходного прямоугольника и по умолчанию позволяет плавно изменять размер.

Чтобы перейти на библиотеку Jetpack, замените существующую реализацию режима "Картинка в картинке" на API библиотеки Jetpack. Сложность и стоимость внедрения будут зависеть от текущей реализации приложения.

В следующих разделах описаны некоторые типичные сценарии использования режима "Картинка в картинке" и необходимые шаги по его реализации.

Приложение сообщает библиотеке о том, активно ли состояние навигации, и задает соотношение сторон. Остальное сделает библиотека Jetpack.

Основные различия

  1. В приложении не нужно различать автоматический и обычный вход.
  2. Объединенные интерфейсы обратного вызова.
  3. Новый конструктор PictureInPictureParams для обратной совместимости.

Видеовызов

Приложение сообщает библиотеке о том, активен ли вызов, и задает соотношение сторон.

Основные различия

  1. В приложении не нужно различать автоматический и обычный вход.
  2. Объединенные интерфейсы обратного вызова.
  3. Новый конструктор PictureInPictureParams для обратной совместимости.
  4. Стандартизированные значки действий для видеовызова.

5. Перенос кода

  • Логика входа. Замените логику, относящуюся к API, например setAutoEnterEnabled для Android 12 и более поздних версий или onUserLeaveHint для Android 11 и более ранних версий, на setEnabled. Запускайте его каждый раз, когда меняется статус соответствия требованиям для режима "Картинка в картинке".
  • Обратные вызовы. Объедините onPictureInPictureModeChanged (переключение макета) и onPictureInPictureUiStateChanged (анимация/состояния) в единый обратный вызов на основе событий onPictureInPictureEvent.
  • Действия и параметры. Обновляйте параметры с помощью setActions и setAspectRatio в экземпляре шаблона при каждом изменении.

Шаблоны реализации

Примеры реализации.

Навигация и видеовызовы

class NavOrVideoCallJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: BasicPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = BasicPictureInPicture(this)
        // BasicPictureInPicture is ideal for Navigation and Video call use cases.
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTERED -> { /* Toggle to PiP layout */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Toggle to Full-screen layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* Optional: PiP is stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* Optional: PiP is unstashed */ }
        }
    }
}

Неполадки при воспроизведении

class VideoPlaybackJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: VideoPlaybackPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = VideoPlaybackPictureInPicture(this)
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
            ContentScreen(pictureInPictureImpl)
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTER_ANIMATION_START -> { /* Hide overlays */ }
            PictureInPictureDelegate.Event.ENTER_ANIMATION_END -> { /* Animation finished */ }
            PictureInPictureDelegate.Event.ENTERED -> { /* Switch to PiP layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* PiP stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* PiP unstashed */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Return to full-screen */ }
        }
    }

    @Composable
    fun ContentScreen(pipController: VideoPlaybackPictureInPicture) {
        DisposableEffect(pipController) {
            onDispose {
                pipController.close()
            }
        }
    }
}