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