Picture-in-picture (PiP) adalah jenis khusus mode multi-aplikasi yang sebagian besar digunakan untuk pemutaran video. Fitur ini memungkinkan pengguna menonton video di jendela kecil yang disematkan ke sudut layar sambil beralih antar-aplikasi atau menjelajahi konten di layar utama.
PiP memanfaatkan API multi-aplikasi yang tersedia di Android 7.0 untuk menyediakan jendela overlay video yang disematkan. Untuk menambahkan PiP ke aplikasi, Anda harus mendaftarkan aktivitas, mengalihkan aktivitas ke mode PiP sesuai kebutuhan, dan memastikan elemen UI disembunyikan dan pemutaran video berlanjut saat aktivitas dalam mode PiP.
Panduan ini menjelaskan cara menambahkan PiP di Compose ke aplikasi Anda dengan implementasi video Compose. Lihat aplikasi Socialite untuk melihat penerapan praktik terbaik ini.
Siapkan aplikasi untuk PiP
Di tag aktivitas file AndroidManifest.xml
Anda, lakukan hal berikut:
- Tambahkan
supportsPictureInPicture
dan setel ketrue
untuk menyatakan bahwa Anda akan menggunakan PiP di aplikasi. Tambahkan
configChanges
dan setel keorientation|screenLayout|screenSize|smallestScreenSize
untuk menentukan bahwa aktivitas Anda menangani perubahan konfigurasi tata letak. Dengan cara ini, aktivitas Anda tidak diluncurkan kembali saat perubahan tata letak terjadi selama transisi mode PiP.<activity android:name=".SnippetsActivity" android:exported="true" android:supportsPictureInPicture="true" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" android:theme="@style/Theme.Snippets">
Di kode Compose, lakukan hal berikut:
- Tambahkan ekstensi ini di
Context
. Anda akan menggunakan ekstensi ini beberapa kali di sepanjang panduan untuk mengakses aktivitas tersebut.internal fun Context.findActivity(): ComponentActivity { var context = this while (context is ContextWrapper) { if (context is ComponentActivity) return context context = context.baseContext } throw IllegalStateException("Picture in picture should be called in the context of an Activity") }
Menambahkan PiP di aplikasi keluar dari aplikasi untuk pra-Android 12
Untuk menambahkan PiP untuk pra-Android 12, gunakan addOnUserLeaveHintProvider
. Ikuti
langkah-langkah berikut guna menambahkan PiP untuk pra-Android 12:
- Tambahkan gate versi sehingga kode ini hanya diakses di versi O hingga R.
- Gunakan
DisposableEffect
denganContext
sebagai kunci. - Di dalam
DisposableEffect
, tentukan perilaku saatonUserLeaveHintProvider
dipicu menggunakan lambda. Di lambda, panggilenterPictureInPictureMode()
padafindActivity()
dan teruskanPictureInPictureParams.Builder().build()
. - Tambahkan
addOnUserLeaveHintListener
menggunakanfindActivity()
dan teruskan lambda. - Di
onDispose
, tambahkanremoveOnUserLeaveHintListener
menggunakanfindActivity()
dan teruskan lambda.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S ) { val context = LocalContext.current DisposableEffect(context) { val onUserLeaveBehavior: () -> Unit = { context.findActivity() .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) } context.findActivity().addOnUserLeaveHintListener( onUserLeaveBehavior ) onDispose { context.findActivity().removeOnUserLeaveHintListener( onUserLeaveBehavior ) } } } else { Log.i(PIP_TAG, "API does not support PiP") }
Menambahkan PiP di aplikasi keluar untuk setelah Android 12
Setelah Android 12, PictureInPictureParams.Builder
ditambahkan melalui
pengubah yang diteruskan ke pemutar video aplikasi.
- Buat
modifier
dan panggilonGloballyPositioned
di dalamnya. Koordinat tata letak akan digunakan di langkah selanjutnya. - Buat variabel untuk
PictureInPictureParams.Builder()
. - Tambahkan pernyataan
if
untuk memeriksa apakah SDK adalah S atau yang lebih tinggi. Jika demikian, tambahkansetAutoEnterEnabled
ke builder dan setel ketrue
untuk masuk ke mode PiP setelah menggeser. Hal ini akan memberikan animasi yang lebih halus daripada melaluienterPictureInPictureMode
. - Gunakan
findActivity()
untuk memanggilsetPictureInPictureParams()
. Panggilbuild()
padabuilder
dan teruskan.
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(true) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
Menambahkan PiP melalui tombol
Untuk masuk ke mode PiP melalui klik tombol, panggil
enterPictureInPictureMode()
di findActivity()
.
Parameter sudah ditetapkan oleh panggilan sebelumnya ke
PictureInPictureParams.Builder
, sehingga Anda tidak perlu menetapkan parameter baru
di builder. Namun, jika ingin mengubah parameter apa pun pada klik tombol, Anda dapat menetapkannya di sini.
val context = LocalContext.current Button(onClick = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.findActivity().enterPictureInPictureMode( PictureInPictureParams.Builder().build() ) } else { Log.i(PIP_TAG, "API does not support PiP") } }) { Text(text = "Enter PiP mode!") }
Menangani UI dalam mode PiP
Saat Anda memasuki mode PiP, seluruh UI aplikasi akan memasuki jendela PiP, kecuali jika Anda menentukan bagaimana UI akan terlihat ke dalam dan ke luar mode PiP.
Pertama, Anda perlu mengetahui kapan aplikasi dalam mode PiP atau tidak. Anda dapat menggunakan
OnPictureInPictureModeChangedProvider
untuk melakukannya.
Kode di bawah ini akan memberi tahu apakah aplikasi dalam mode PiP.
@Composable fun rememberIsInPipMode(): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val activity = LocalContext.current.findActivity() var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) } DisposableEffect(activity) { val observer = Consumer<PictureInPictureModeChangedInfo> { info -> pipMode = info.isInPictureInPictureMode } activity.addOnPictureInPictureModeChangedListener( observer ) onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } } return pipMode } else { return false } }
Sekarang, Anda dapat menggunakan rememberIsInPipMode()
untuk beralih elemen UI mana yang akan ditampilkan
saat aplikasi memasuki mode PiP:
val inPipMode = rememberIsInPipMode() Column(modifier = modifier) { // This text will only show up when the app is not in PiP mode if (!inPipMode) { Text( text = "Picture in Picture", ) } VideoPlayer() }
Pastikan aplikasi Anda memasuki mode PiP pada waktu yang tepat
Aplikasi Anda tidak boleh memasuki mode PiP dalam situasi berikut:
- Apakah video dihentikan atau dijeda.
- Jika Anda berada di halaman aplikasi yang berbeda dengan pemutar video.
Untuk mengontrol kapan aplikasi memasuki mode PiP, tambahkan variabel yang melacak status
pemutar video menggunakan mutableStateOf
.
Beralih status berdasarkan apakah video diputar
Untuk beralih status berdasarkan apakah pemutar video sedang diputar, tambahkan pemroses di pemutar video. Ubah status variabel status berdasarkan apakah pemutar sedang diputar atau tidak:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
Beralih status berdasarkan apakah pemutar dilepaskan
Saat pemutar dirilis, tetapkan variabel status ke false
:
fun releasePlayer() { shouldEnterPipMode = false }
Gunakan status untuk menentukan apakah mode PiP dimasukkan (sebelum Android 12)
- Karena penambahan PiP pra-12 menggunakan
DisposableEffect
, Anda perlu membuat variabel baru denganrememberUpdatedState
dengannewValue
ditetapkan sebagai variabel status. Tindakan ini akan memastikan bahwa versi terbaru digunakan dalamDisposableEffect
. Pada lambda yang menentukan perilaku saat
OnUserLeaveHintListener
dipicu, tambahkan pernyataanif
dengan variabel status di sekitar panggilan keenterPictureInPictureMode()
:val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S ) { val context = LocalContext.current DisposableEffect(context) { val onUserLeaveBehavior: () -> Unit = { if (currentShouldEnterPipMode) { context.findActivity() .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) } } context.findActivity().addOnUserLeaveHintListener( onUserLeaveBehavior ) onDispose { context.findActivity().removeOnUserLeaveHintListener( onUserLeaveBehavior ) } } } else { Log.i(PIP_TAG, "API does not support PiP") }
Gunakan status untuk menentukan apakah mode PiP dimasukkan (setelah Android 12)
Teruskan variabel status Anda ke setAutoEnterEnabled
agar aplikasi hanya memasuki
mode PiP pada waktu yang tepat:
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() // Add autoEnterEnabled for versions S and up if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
Menggunakan setSourceRectHint
untuk menerapkan animasi yang lancar
setSourceRectHint
API membuat animasi yang lebih lancar untuk memasuki mode
PiP. Di Android 12+, terdapat juga animasi yang lebih halus untuk keluar dari mode PiP.
Tambahkan API ini ke builder PiP untuk menunjukkan area aktivitas yang
terlihat setelah transisi ke PiP.
- Hanya tambahkan
setSourceRectHint()
kebuilder
jika status menentukan bahwa aplikasi harus masuk ke mode PiP. Tindakan ini akan menghindari penghitungansourceRect
saat aplikasi tidak perlu memasukkan PiP. - Untuk menetapkan nilai
sourceRect
, gunakanlayoutCoordinates
yang diberikan dari fungsionGloballyPositioned
di pengubah. - Panggil
setSourceRectHint()
padabuilder
dan teruskan variabelsourceRect
.
val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (shouldEnterPipMode) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
Gunakan setAspectRatio
untuk menyetel rasio aspek jendela PiP
Untuk menyetel rasio aspek jendela PiP, Anda dapat memilih rasio aspek
tertentu atau menggunakan lebar dan tinggi ukuran video pemutar. Jika Anda
menggunakan pemutar media3, pastikan bahwa pemutar bukan null dan ukuran
video pemutar tidak sama dengan VideoSize.UNKNOWN
sebelum menetapkan rasio
lebar tinggi.
val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) builder.setAspectRatio( Rational(player.videoSize.width, player.videoSize.height) ) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
Jika Anda menggunakan pemutar kustom, tetapkan rasio aspek tinggi dan lebar pemain menggunakan sintaksis khusus untuk pemutar Anda. Perlu diketahui bahwa jika ukuran pemutar berubah selama inisialisasi, jika berada di luar batas yang valid dari rasio aspek, aplikasi Anda akan error. Anda mungkin perlu menambahkan pemeriksaan kapan rasio aspek dapat dihitung, mirip dengan cara yang dilakukan untuk pemutar media3.
Menambahkan tindakan jarak jauh
Jika ingin menambahkan kontrol (putar, jeda, dll.) ke jendela PiP, buat
RemoteAction
untuk setiap kontrol yang ingin ditambahkan.
- Tambahkan konstanta untuk kontrol siaran Anda:
// Constant for broadcast receiver const val ACTION_BROADCAST_CONTROL = "broadcast_control" // Intent extras for broadcast controls from Picture-in-Picture mode. const val EXTRA_CONTROL_TYPE = "control_type" const val EXTRA_CONTROL_PLAY = 1 const val EXTRA_CONTROL_PAUSE = 2
- Buat daftar
RemoteActions
untuk kontrol di jendela PiP. - Selanjutnya, tambahkan
BroadcastReceiver
dan gantionReceive()
untuk menetapkan tindakan setiap tombol. GunakanDisposableEffect
untuk mendaftarkan penerima dan tindakan jarak jauh. Saat pemutar dibuang, batalkan pendaftaran penerima.@RequiresApi(Build.VERSION_CODES.O) @Composable fun PlayerBroadcastReceiver(player: Player?) { val isInPipMode = rememberIsInPipMode() if (!isInPipMode || player == null) { // Broadcast receiver is only used if app is in PiP mode and player is non null return } val context = LocalContext.current DisposableEffect(player) { val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) { return } when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) { EXTRA_CONTROL_PAUSE -> player.pause() EXTRA_CONTROL_PLAY -> player.play() } } } ContextCompat.registerReceiver( context, broadcastReceiver, IntentFilter(ACTION_BROADCAST_CONTROL), ContextCompat.RECEIVER_NOT_EXPORTED ) onDispose { context.unregisterReceiver(broadcastReceiver) } } }
- Teruskan daftar tindakan jarak jauh ke
PictureInPictureParams.Builder
:val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() builder.setActions( listOfRemoteActions() ) if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) builder.setAspectRatio( Rational(player.videoSize.width, player.videoSize.height) ) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(modifier = pipModifier)
Langkah berikutnya
Dalam panduan ini, Anda telah mempelajari praktik terbaik untuk menambahkan PiP di Compose baik sebelum Android 12 maupun setelah Android 12.
- Lihat aplikasi Socialite untuk melihat cara kerja praktik terbaik PiP Compose.
- Lihat panduan desain PiP untuk mengetahui informasi selengkapnya.