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