Реализация Picture-in-Picture режима для Android-приложения
PiP на Android появился в API 26 (Android 8.0) и за несколько версий заметно эволюционировал. На Android 12+ появились автоматический PiP при свайпе домой и seamless resize. Но базовые ошибки при реализации — те же: Activity не восстанавливается корректно, управляющие кнопки не работают, видео зависает после выхода из PiP.
Включение PiP
В AndroidManifest.xml для Activity:
<activity
android:name=".VideoActivity"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />
configChanges — критично. Без этого при входе в PiP Activity пересоздаётся (onDestroy → onCreate), видео прерывается.
Вход в PiP — через enterPictureInPictureMode:
override fun onUserLeaveHint() {
// автоматически при нажатии Home — API 26+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.build()
enterPictureInPictureMode(params)
}
}
На Android 12+ (API 31): setAutoEnterEnabled(true) в PictureInPictureParams — PiP при свайпе в фон без переопределения onUserLeaveHint. Плюс setSeamlessResizeEnabled(true) для плавного изменения размера окна.
Lifecycle и управление воспроизведением
Когда Activity переходит в PiP, вызывается onPictureInPictureModeChanged. В PiP-режиме UI-контролы должны быть скрыты, плеер должен продолжать воспроизведение.
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
videoControlsView.visibility = View.GONE
// ExoPlayer продолжает играть — ничего не делаем
} else {
videoControlsView.visibility = View.VISIBLE
// пользователь вернулся — восстановить UI
}
}
Проблема с onResume / onPause: при переходе в PiP вызывается onPause. Если в onPause стоит player.pause() — видео остановится. Проверка isInPictureInPictureMode() в onPause решает это:
override fun onPause() {
super.onPause()
if (!isInPictureInPictureMode) {
player.pause()
}
}
Кастомные Remote Actions
В PiP-окне можно добавить до 3 кнопок через RemoteAction. Например, «Предыдущий трек», «Пауза», «Следующий трек»:
fun buildPipParams(isPlaying: Boolean): PictureInPictureParams {
val playPauseIntent = PendingIntent.getBroadcast(
this, REQUEST_PLAY_PAUSE,
Intent(ACTION_PLAY_PAUSE),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val playPauseAction = RemoteAction(
Icon.createWithResource(this, if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play),
if (isPlaying) "Пауза" else "Воспроизвести",
if (isPlaying) "Пауза" else "Воспроизвести",
playPauseIntent
)
return PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.setActions(listOf(playPauseAction))
.build()
}
BroadcastReceiver обрабатывает ACTION_PLAY_PAUSE и после обработки обновляет параметры PiP через setPictureInPictureParams для актуализации иконки кнопки.
Видеозвонки и кастомный контент
Для PiP с произвольным View (не только видео) — тот же подход: Activity в PiP-режиме с кастомным layout. Отличие от iOS: на Android нет ограничения «только видеозвонок» — любой контент может быть в PiP. WebRTC, камера, игровой экран — всё работает.
Для WebRTC-видеозвонка: SurfaceViewRenderer из org.webrtc помещается в Activity, при переходе в PiP скрываются контролы вызова, остаётся только видео. EglRenderer продолжает рендеринг в PiP-режиме без изменений.
Частые проблемы
Activity пересоздаётся при PiP. Забытый configChanges в манифесте.
Чёрный экран в PiP при ExoPlayer. SurfaceView иногда не корректно масштабируется в маленьком PiP-окне. Заменить на TextureView через playerView.useController = false и SimpleExoPlayer.setVideoTextureView.
PiP не работает на MIUI / ColorOS. Производители могут ограничивать PiP в своих оболочках. На MIUI нужно отдельное разрешение в настройках приложения. Тестировать на реальных устройствах, не только на эмуляторе.
Сроки
2–3 рабочих дня для стандартной интеграции с ExoPlayer. Кастомные Remote Actions и сложный lifecycle с видеозвонком — до 4–5 дней. Стоимость рассчитывается индивидуально.







