Реалізація 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 днів. Вартість розраховується індивідуально.







