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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *