Главное фото (заменить на свое)
Главное фото (заменить на свое)

Подготовка

Я пропущу настройку самого видео, чтобы не грузить статью лишней информацией.
Пример настроенного приложения с видео можете клонировать отсюда (ветка 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()
        }
    }
}

Комментарии (0)