Picture-in-picture (PiP) adalah jenis khusus dari mode multi-aplikasi yang banyak digunakan untuk pemutaran video. Pengguna dapat menonton video di jendela kecil yang disematkan ke pojok layar saat bernavigasi 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, alihkan aktivitas ke mode PiP sesuai kebutuhan, dan pastikan elemen UI disembunyikan dan pemutaran video dilanjutkan saat aktivitas dalam mode PiP.
Panduan ini menjelaskan cara menambahkan PiP di Compose ke aplikasi dengan video Compose terlepas dari implementasi layanan. Lihat aplikasi Socialite untuk melihat cara kerja praktik terbaik ini.
Menyiapkan aplikasi untuk PiP
Di tag aktivitas file AndroidManifest.xml
Anda, lakukan hal berikut:
- Tambahkan
supportsPictureInPicture
dan tetapkan ketrue
untuk menyatakan bahwa Anda akan menggunakan PiP di aplikasi. Tambahkan
configChanges
dan tetapkan 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">
Dalam kode Compose, lakukan hal berikut:
- Tambahkan ekstensi ini di
Context
. Anda akan menggunakan ekstensi ini beberapa kali di sepanjang panduan untuk mengakses aktivitas ini.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 pergi untuk versi Android sebelum 12
Untuk menambahkan PiP untuk pra-Android 12, gunakan addOnUserLeaveHintProvider
. Ikuti
langkah-langkah berikut untuk menambahkan PiP untuk versi Android sebelum 12:
- Tambahkan gate versi sehingga kode ini hanya diakses dalam versi O hingga R.
- Gunakan
DisposableEffect
denganContext
sebagai kuncinya. - Di dalam
DisposableEffect
, tentukan perilaku saatonUserLeaveHintProvider
dipicu menggunakan lambda. Di lambda, panggilenterPictureInPictureMode()
difindActivity()
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 info", "API does not support PiP") }
Menambahkan PiP di aplikasi keluar untuk perangkat setelah Android 12
Setelah Android 12, PictureInPictureParams.Builder
ditambahkan melalui
yang diteruskan ke pemutar video aplikasi.
- Buat
modifier
dan panggilonGloballyPositioned
di dalamnya. Tata letak koordinat akan digunakan di langkah selanjutnya. - Buat variabel untuk
PictureInPictureParams.Builder()
. - Tambahkan pernyataan
if
untuk memeriksa apakah SDK adalah S atau yang lebih baru. Jika ya, tambahkansetAutoEnterEnabled
ke builder dan tetapkan ketrue
untuk masuk ke PiP saat digeser. Ini 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 dengan mengklik tombol, panggil
enterPictureInPictureMode()
di findActivity()
.
Parameter sudah ditetapkan oleh panggilan sebelumnya ke
PictureInPictureParams.Builder
, sehingga Anda tidak perlu menetapkan parameter baru
pada builder. Namun, jika ingin mengubah parameter apa pun saat tombol diklik, 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 memasuki mode PiP, seluruh UI aplikasi akan memasuki jendela PiP, kecuali jika Anda menentukan tampilan UI Anda di dalam dan di luar mode PiP.
Pertama, Anda perlu mengetahui kapan aplikasi Anda dalam mode PiP atau tidak. Anda dapat menggunakan
OnPictureInPictureModeChangedProvider
untuk mencapainya.
Kode di bawah memberi tahu Anda 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 menampilkan/menyembunyikan elemen UI 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 tidak boleh memasuki mode PiP dalam situasi berikut:
- Jika 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 sedang diputar atau tidak
Untuk mengalihkan status berdasarkan apakah pemutar video sedang diputar, tambahkan pemroses di pemutar video. Alihkan status variabel status Anda berdasarkan apakah pemutar sedang diputar atau tidak:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
Beralih status berdasarkan apakah pemutar dirilis
Saat pemutar dirilis, tetapkan variabel status Anda ke false
:
fun releasePlayer() { shouldEnterPipMode = false }
Gunakan status untuk menentukan apakah mode PiP dimasukkan (pra-Android 12)
- Karena penambahan PiP untuk pra-12 menggunakan
DisposableEffect
, Anda harus membuat variabel baru olehrememberUpdatedState
dengannewValue
ditetapkan sebagai variabel status. Hal ini akan memastikan bahwa versi terbaru digunakan dalamDisposableEffect
. Di lambda yang menentukan perilaku saat
OnUserLeaveHintListener
dipicu, tambahkan pernyataanif
dengan variabel status di sekitar panggilan untukenterPictureInPictureMode()
: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 info", "API does not support PiP") }
Menggunakan status untuk menentukan apakah mode PiP dimasukkan (pasca-Android 12)
Teruskan variabel status Anda ke setAutoEnterEnabled
sehingga aplikasi Anda hanya masuk
Mode PiP pada saat 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 mulus
setSourceRectHint
API membuat animasi yang lebih halus untuk memasuki PiP
mode. Di Android 12+, tindakan ini juga membuat 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 akan memasuki mode PiP. Hal ini akan menghindari penghitungansourceRect
saat aplikasi tidak perlu masuk ke dalam PiP. - Untuk menetapkan nilai
sourceRect
, gunakanlayoutCoordinates
yang diberikan dari fungsionGloballyPositioned
pada pengubah. - Panggil
setSourceRectHint()
padabuilder
dan teruskansourceRect
variabel.
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 atau gunakan lebar dan tinggi ukuran video pemutar. Jika Anda
menggunakan pemutar media3, periksa apakah pemutar
bukan {i>null <i}dan bahwa antarmuka
ukuran video tidak sama dengan VideoSize.UNKNOWN
sebelum menetapkan aspek
rasio.
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 pada tinggi pemutar dan lebar menggunakan sintaks yang spesifik untuk pemutar Anda. Perhatikan bahwa jika pemain diubah ukurannya selama inisialisasi, jika berada di luar batas yang valid dari rasio aspeknya, aplikasi Anda akan error. Anda mungkin perlu menambahkan pemeriksaan saat rasio aspek dapat dihitung, mirip dengan yang dilakukan pada web.
Menambahkan tindakan jarak jauh
Jika Anda 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 menyetel 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 Anda 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 sebelum Android 12 dan setelah Android 12.
- Lihat aplikasi Sosialit untuk melihat praktik terbaik Cara kerja PiP Compose.
- Lihat panduan desain PiP untuk mengetahui informasi selengkapnya.