Menambahkan picture-in-picture (PiP) ke aplikasi dengan pemutar video Compose

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:

  1. Tambahkan supportsPictureInPicture dan setel ke true untuk menyatakan bahwa Anda akan menggunakan PiP di aplikasi.
  2. Tambahkan configChanges dan setel ke orientation|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:

  1. 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:

  1. Tambahkan gate versi sehingga kode ini hanya diakses di versi O hingga R.
  2. Gunakan DisposableEffect dengan Context sebagai kunci.
  3. Di dalam DisposableEffect, tentukan perilaku saat onUserLeaveHintProvider dipicu menggunakan lambda. Di lambda, panggil enterPictureInPictureMode() pada findActivity() dan teruskan PictureInPictureParams.Builder().build().
  4. Tambahkan addOnUserLeaveHintListener menggunakan findActivity() dan teruskan lambda.
  5. Di onDispose, tambahkan removeOnUserLeaveHintListener menggunakan findActivity() 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.

  1. Buat modifier dan panggil onGloballyPositioned di dalamnya. Koordinat tata letak akan digunakan di langkah selanjutnya.
  2. Buat variabel untuk PictureInPictureParams.Builder().
  3. Tambahkan pernyataan if untuk memeriksa apakah SDK adalah S atau yang lebih tinggi. Jika demikian, tambahkan setAutoEnterEnabled ke builder dan setel ke true untuk masuk ke mode PiP setelah menggeser. Hal ini akan memberikan animasi yang lebih halus daripada melalui enterPictureInPictureMode.
  4. Gunakan findActivity() untuk memanggil setPictureInPictureParams(). Panggil build() pada builder 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)

  1. Karena penambahan PiP pra-12 menggunakan DisposableEffect, Anda perlu membuat variabel baru dengan rememberUpdatedState dengan newValue ditetapkan sebagai variabel status. Tindakan ini akan memastikan bahwa versi terbaru digunakan dalam DisposableEffect.
  2. Pada lambda yang menentukan perilaku saat OnUserLeaveHintListener dipicu, tambahkan pernyataan if dengan variabel status di sekitar panggilan ke enterPictureInPictureMode():

    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.

  1. Hanya tambahkan setSourceRectHint() ke builder jika status menentukan bahwa aplikasi harus masuk ke mode PiP. Tindakan ini akan menghindari penghitungan sourceRect saat aplikasi tidak perlu memasukkan PiP.
  2. Untuk menetapkan nilai sourceRect, gunakan layoutCoordinates yang diberikan dari fungsi onGloballyPositioned di pengubah.
  3. Panggil setSourceRectHint() pada builder dan teruskan variabel sourceRect.

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 menetapkan rasio aspek jendela PiP, Anda dapat memilih rasio aspek tertentu atau menggunakan lebar dan tinggi variabel sourceRect untuk menetapkan rasio aspek.

val context = LocalContext.current

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()

    if (shouldEnterPipMode) {
        val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
        builder.setSourceRectHint(sourceRect)
        builder.setAspectRatio(
            Rational(sourceRect.width(), sourceRect.height())
        )
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

Menambahkan tindakan jarak jauh

Jika ingin menambahkan kontrol (putar, jeda, dll.) ke jendela PiP, buat RemoteAction untuk setiap kontrol yang ingin ditambahkan.

  1. 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
  2. Buat daftar RemoteActions untuk kontrol di jendela PiP.
  3. Selanjutnya, tambahkan BroadcastReceiver dan ganti onReceive() untuk menetapkan tindakan setiap tombol. Gunakan DisposableEffect 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)
            }
        }
    }
  4. 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) {
            val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
            builder.setSourceRectHint(sourceRect)
            builder.setAspectRatio(
                Rational(sourceRect.width(), sourceRect.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.