Picture in Picture Mode в android. Показываем видео в мини-окне

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Главное фото (заменить на свое)
Главное фото (заменить на свое)

Подготовка

Я пропущу настройку самого видео, чтобы не грузить статью лишней информацией.
Пример настроенного приложения с видео можете клонировать отсюда (ветка video_player_added)

Начнем с этого
Начнем с этого

Ограничения

Picture in Picture (PiP) mode появился в android 8.0 (api level 26). Проверить версию можно так:

(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)

На устройствах с маленьким объемом оперативки PiP-mode также может быть недоступен. Проверить доступность PiP-mode можно так:

context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)

Настройка activity

Внесем изменения в AndroidManifest.xml

<activity
    ...
    android:supportsPictureInPicture="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">

android:supportsPictureInPicture - флаг, показывающий что activity поддерживает PiP-mode
android:configChanges - список типов изменений конфигурации, которые activity обрабатывает сама. По умолчанию, activity пересоздается, когда происходит смена конфигурации. Если же указаны configChanges, то у activity лишь вызовется метод Activity.onConfigurationChanged() и она пересоздаваться не будет.

Переход в PiP-mode

Переход activity в PiP-mode происходит через вызов метода Activity.enterPictureInPictureMode(PictureInPictureParams params)

Есть два варианта перехода в PiP-mode:

  • Вызов enterPictureInPictureMode() в OnClickListener какой-либо кнопки

enterPipButton.setOnClickListener {
    enterPictureInPictureMode(params)
}
  • Вызов enterPictureInPictureMode() внутри Activity.onUserLeaveHint()
    onUserLeaveHint() - метод, который вызывается когда activity уходит в фон только из-за действий пользователя.
    Например, если пользователь нажал кнопку Home, то onUserLeaveHint() будет вызван. Но если нашу activity перекроет экран входящего звонка, то onUserLeaveHint() вызван не будет.

override fun onUserLeaveHint() {
    super.onUserLeaveHint()

    enterPictureInPictureMode(params)
}

Также начиная с android 12 (api level 31) можно проставить флаг PictureInPictureParams.setAutoEnterEnabled, который автоматически будет переводить activity в PiP-mode при ее сворачивании.

PiP-mode параметры

Настройка PiP-mode происходит через PictureInPictureParams.
Вот основные параметры:

  • setAspectRatio(aspectRatio) - устанавливает соотношение сторон (ширина/высота)

Примеры aspect ratio
Примеры aspect ratio
  • setSourceRectHint(sourceRectHint) - границы контента, который будет виден во время перехода в PiP-mode. Для лучшего эффекта sourceRectHint должен соответствовать aspectRatio.
    Заметьте! При завершении перехода в PiP-mode границы контента будут пересчитаны от верхней границы activity в соответствии с aspectRatio.

  • setAutoEnterEnabled(true) - описанный выше флаг, который говорит о том, что при сворачивании activity ее нужно показать в PiP-mode. Доступен с android 12 (api level 31)

  • setActions(actions) - добавляет кнопки взаимодействия с activity в PiP-mode. Подробнее будет описано ниже

Обработка UI в PiP-mode

Когда activity перешло в PiP-mode, с ней уже нельзя взаимодействовать. По сути activity переходит в состояние onPause(). Поэтому нам нужно скрыть все ненужные кнопки, контролы и просто мелкие элементы (их все равно будет не видно)

Понять что activity перешло в PiP-mode можно внутри методаActivity.onPictureInPictureModeChanged().

override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,
                                           newConfig: Configuration) {
    if (isInPictureInPictureMode) {
        // Скрываем лишние кнопки, мелкие элементы итд
    } else {
        // Восстанавлиаем состояние ui
    }
}

Взаимодействие с activity в PiP-mode

Чтобы взаимодействовать с activity нам нужно добавить RemoteAction.

val icon = Icon.createWithResource(context, R.drawable.play)
val intent = PiPModeActionsReceiver.createPlayIntent(this)

//Не буду останавливаться на теме PendingIntent, чтобы не перегружать статью
val pendingIntent = PendingIntent.getBroadcast(context, 1, intent, PendingIntent.FLAG_IMMUTABLE)

val action = RemoteAction(
    icon,
    "Play",
    "Play video",
    pendingIntent
)

Принимать события нажатия на RemoteAction мы будем через BroadcastReceiver.
Напишем его реализацию:

class PiPModeActionsReceiver(
    private val pipModeActionsListener: PiPModeActionsListener
) : BroadcastReceiver() {

    companion object {
        private const val ACTION = "pip_mode_action"
        private const val EXTRA_CONTROL_TYPE = "control_type"
        private const val REQUEST_PLAY = 1
        private const val REQUEST_PAUSE = 2

        fun createPlayIntent(context: Context): Intent {
            val intent = Intent(context, PiPModeActionsReceiver::class.java)
            intent.putExtra(EXTRA_CONTROL_TYPE, REQUEST_PLAY)
            return intent
        }

        fun createPauseIntent(context: Context): Intent {
            val intent = Intent(context, PiPModeActionsReceiver::class.java)
            intent.putExtra(EXTRA_CONTROL_TYPE, REQUEST_PAUSE)
            return intent
        }
    }

    override fun onReceive(context: Context, intent: Intent) {
        when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
            REQUEST_PAUSE -> pipModeActionsListener.onPauseClick()
            REQUEST_PLAY -> pipModeActionsListener.onPlayClick()
        }
    }
}
interface PiPModeActionsListener {
    fun onPlayClick()
    fun onPauseClick()
}
class MainActivity : AppCompatActivity(), PiPModeActionsListener {

  ...

  override fun onPauseClick() {
      playerControlView.player?.pause()
      val params = paramsBuilder
          .setActions(getPlayAction())
          .build()
      setPictureInPictureParams(params)
  }
  override fun onPlayClick() {
      playerControlView.player?.play()
      val params = paramsBuilder
          .setActions(getPauseAction())
          .build()
      setPictureInPictureParams(params)
  }
}

Activity lifecycle в PiP-mode

BackStack activity в PiP-mode

Как вы уже заметили, enterPictureInPictureMode() переводит в PiP-mode только текущую activity. Если же ваше приложение построено на подходе multi-activity, то могут возникнуть различные баги c backstack'ом activity.
Это связано с тем, что после выхода из PiP-mode activity покажется в новом Task (подробнее про activities task). Пример на видео:

Чтобы исправить эту проблему добавим launchMode к нашей activity в AndroidManifest.xml.

<activity
    ...
    android:launchMode="singleTask">

Если коротко, то при этом launchMode у нас в task'е может быть только один экземпляр данной activity (подробнее про launchMode)

Выход из PiP-mode

Неочевидным может стать то, что activity в PiP-mode после закрытия не уничтожается, а переходит в свернутое состояние (onPause).
Это можно исправить, например, вот так:

  • До android 5.0 (api level 21)

class MainActivity : AppCompatActivity() {
    
    ...

    override fun onStop() {
        super.onStop()
        if (isInPictureInPictureMode) {
            finish()
        }
    }
}
//AndroidManifest.xml
<activity
    ...
    android:autoRemoveFromRecents="true">
  • После android 5.0 (api level 21)

class MainActivity : AppCompatActivity() {
    
    ...

    override fun onStop() {
        super.onStop()
        if (isInPictureInPictureMode) {
            finishAndRemoveTask()
        }
    }
}

Источник: https://habr.com/ru/articles/734890/


Интересные статьи

Интересные статьи

Некоторые люди полагаются на зрение для распознавания личности собеседника. Это крайне ненадёжный подход, если можно подделать картинку на экране. В эпоху цифровой видеосвязи внешность, голос и по...
6 января 2021 года толпа протестующих захватила Капитолий США. Несколько десятков проникли внутрь здания в места, обычно закрытые для публики, в том числе в кабинеты парламентариев и ...
Неделю назад прошел Live! Badoo Localization Meetup. Для нас это первая встреча на такую тему, да еще и в онлайн-формате. Отдельные секции по локализации редко встречаются на конференциях, по...
Когда один из наших сотрудников сказал своему знакомому сисадмину: «А у нас теперь новая услуга — VDS с видеокартой», тот в ответ усмехнулся: «Что, будете офисную братию на майнинг толкать?». Ну ...
Дискретная видеокарта начального уровня Intel Graphics Xe, официальный анонс состоялся 20 марта на игровой конференции GDC 2019 Компания Intel опубликовала документацию на графические процес...