Hình trong hình (PiP) là một loại đặc biệt của chế độ nhiều cửa sổ được dùng chủ yếu khi phát lại video. Tính năng này cho phép người dùng xem video trong một cửa sổ nhỏ được ghim vào góc màn hình trong khi di chuyển giữa các ứng dụng hoặc duyệt xem nội dung trên màn hình chính.
PiP tận dụng các API nhiều cửa sổ có trong Android 7.0 để cung cấp cửa sổ lớp phủ của video đã ghim. Để thêm tính năng Hình trong hình vào ứng dụng, bạn cần đăng ký hoạt động, chuyển hoạt động của bạn sang chế độ Hình trong hình nếu cần và đảm bảo các thành phần trên giao diện người dùng sẽ bị ẩn và video sẽ tiếp tục phát khi hoạt động này đang ở chế độ Hình trong hình.
Hướng dẫn này mô tả cách thêm tính năng Hình trong hình trong Compose vào ứng dụng bằng video trong Compose trong quá trình triển khai. Hãy xem ứng dụng Socialite để biết các phương pháp hay nhất này hoạt động như thế nào.
Thiết lập ứng dụng để bật tính năng PiP (Hình trong hình)
Trong thẻ hoạt động của tệp AndroidManifest.xml
, hãy làm như sau:
- Thêm
supportsPictureInPicture
và đặt thànhtrue
để khai báo rằng bạn sẽ sử dụng tính năng PiP trong ứng dụng. Thêm
configChanges
rồi đặt thànhorientation|screenLayout|screenSize|smallestScreenSize
để chỉ định rằng hoạt động của bạn xử lý các thay đổi về cấu hình bố cục. Bằng cách này, hoạt động của bạn sẽ không chạy lại khi các thay đổi về bố cục xảy ra trong quá trình chuyển đổi chế độ PiP.<activity android:name=".SnippetsActivity" android:exported="true" android:supportsPictureInPicture="true" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" android:theme="@style/Theme.Snippets">
Trong mã Compose, hãy làm như sau:
- Thêm tiện ích này trên
Context
. Bạn sẽ sử dụng tiện ích này nhiều lần trong suốt hướng dẫn để truy cập vào hoạt động đó.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") }
Thêm tính năng Hình trong hình khi thoát ứng dụng trên Android 12
Để thêm tính năng Hình trong hình cho những phiên bản trước Android 12, hãy dùng addOnUserLeaveHintProvider
. Theo dõi
sau đây để thêm PiP đối với Android 12 trở về trước:
- Thêm cổng phiên bản để chỉ truy cập được mã này ở các phiên bản O cho đến R.
- Sử dụng
DisposableEffect
vớiContext
làm khoá. - Bên trong
DisposableEffect
, hãy xác định hành vi cho thời điểm kích hoạtonUserLeaveHintProvider
bằng cách sử dụng lambda. Trong hàm lambda, hãy gọienterPictureInPictureMode()
trênfindActivity()
rồi truyền vàoPictureInPictureParams.Builder().build()
. - Thêm
addOnUserLeaveHintListener
bằngfindActivity()
và truyền vào lambda. - Trong
onDispose
, hãy thêmremoveOnUserLeaveHintListener
bằngfindActivity()
và truyền vào hàm 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") }
Thêm PiP khi rời khỏi ứng dụng cho phiên bản Android 12 trở lên
Sau Android 12, PictureInPictureParams.Builder
được thêm thông qua một
đối tượng sửa đổi được chuyển đến trình phát video của ứng dụng.
- Tạo
modifier
rồi gọionGloballyPositioned
trên đó. Bố cục toạ độ sẽ được sử dụng trong bước sau. - Tạo một biến cho
PictureInPictureParams.Builder()
. - Thêm câu lệnh
if
để kiểm tra xem SDK có phải là S trở lên hay không. Nếu có, hãy thêmsetAutoEnterEnabled
vào trình tạo và đặt thànhtrue
để nhập PiP (Hình trong hình) khi vuốt. Cách này giúp ảnh động trở nên mượt mà hơn so với khi xem hếtenterPictureInPictureMode
. - Sử dụng
findActivity()
để gọisetPictureInPictureParams()
. Cuộc gọi chobuild()
trênbuilder
rồi truyền vào.
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)
Thêm PiP thông qua một nút
Để chuyển sang chế độ PiP thông qua một lượt nhấp vào nút, hãy gọi enterPictureInPictureMode()
trên findActivity()
.
Các tham số đã được đặt bởi các lệnh gọi trước đó đối với hàm
PictureInPictureParams.Builder
, nên bạn không cần đặt thông số mới
trên trình tạo. Tuy nhiên, nếu bạn muốn thay đổi bất kỳ tham số nào trên nút
nhấp vào, bạn có thể đặt chúng tại đây.
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!") }
Xử lý giao diện người dùng ở chế độ Hình trong hình
Khi bạn chuyển sang chế độ PiP, toàn bộ giao diện người dùng của ứng dụng sẽ chuyển sang cửa sổ PiP trừ phi bạn chỉ định giao diện người dùng sẽ trông như thế nào cả trong và ngoài chế độ PiP.
Trước tiên, bạn cần biết khi nào ứng dụng của mình có đang ở chế độ PiP hay không. Bạn có thể sử dụng
OnPictureInPictureModeChangedProvider
để đạt được mục tiêu này.
Đoạn mã dưới đây cho bạn biết ứng dụng của bạn có đang ở chế độ PiP hay không.
@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 } }
Giờ đây, bạn có thể sử dụng rememberIsInPipMode()
để bật/tắt những thành phần trên giao diện người dùng sẽ hiển thị
khi ứng dụng chuyển sang chế độ 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() }
Đảm bảo ứng dụng của bạn chuyển sang chế độ PiP vào đúng thời điểm
Ứng dụng của bạn không nên chuyển sang chế độ PiP trong các trường hợp sau:
- Trường hợp video bị dừng hoặc tạm dừng.
- Nếu bạn đang ở một trang khác của ứng dụng chứ không phải trình phát video.
Để kiểm soát thời điểm ứng dụng chuyển sang chế độ PiP, hãy thêm một biến theo dõi trạng thái
của trình phát video bằng mutableStateOf
.
Chuyển đổi trạng thái dựa trên việc video có đang phát hay không
Để bật/tắt trạng thái dựa trên việc trình phát video có đang phát hay không, hãy thêm trình nghe vào trình phát video. Bật/tắt trạng thái của biến trạng thái dựa trên việc người chơi có đang chơi hay không:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
Bật/tắt trạng thái dựa trên việc người chơi có được thả hay không
Khi trình phát được phát hành, hãy đặt biến trạng thái thành false
:
fun releasePlayer() { shouldEnterPipMode = false }
Sử dụng trạng thái để xác định xem bạn đã nhập chế độ PiP hay chưa (trước Android 12)
- Vì việc thêm PiP trước phiên bản 12 sử dụng
DisposableEffect
, nên bạn cần tạo một biến mới bằngrememberUpdatedState
, trong đónewValue
được đặt làm biến trạng thái. Điều này sẽ đảm bảo rằng phiên bản đã cập nhật sẽ được sử dụng trongDisposableEffect
. Trong lambda xác định hành vi khi
OnUserLeaveHintListener
được kích hoạt, hãy thêm câu lệnhif
có biến trạng thái xung quanh lệnh gọi đếnenterPictureInPictureMode()
: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") }
Sử dụng trạng thái để xác định xem đã nhập chế độ PiP hay chưa (sau Android 12)
Truyền biến trạng thái vào setAutoEnterEnabled
để ứng dụng của bạn chỉ nhập
Chế độ PiP vào đúng thời điểm:
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)
Sử dụng setSourceRectHint
để triển khai ảnh động mượt mà
API setSourceRectHint
tạo ảnh động mượt mà hơn để nhập PiP (Hình trong hình)
. Trên Android 12 trở lên, tính năng này cũng tạo ra ảnh động mượt mà hơn khi thoát khỏi chế độ PiP.
Thêm API này vào trình tạo PiP để cho biết khu vực của hoạt động hiển thị sau khi chuyển đổi sang PiP.
- Chỉ thêm
setSourceRectHint()
vàobuilder
nếu trạng thái xác định rằng ứng dụng phải chuyển sang chế độ PiP. Điều này giúp tránh tính toánsourceRect
khi ứng dụng không cần nhập PiP. - Để đặt giá trị
sourceRect
, hãy sử dụnglayoutCoordinates
được cung cấp qua hàmonGloballyPositioned
trên đối tượng sửa đổi. - Gọi
setSourceRectHint()
trênbuilder
rồi truyềnsourceRect
vào biến.
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)
Sử dụng setAspectRatio
để đặt tỷ lệ khung hình của cửa sổ PiP
Để đặt tỷ lệ khung hình của cửa sổ PiP, bạn có thể chọn một tỷ lệ khung hình
tỷ lệ khung hình hoặc sử dụng chiều rộng và chiều cao cho kích thước video của trình phát. Nếu bạn
bằng cách sử dụng trình phát media3, hãy kiểm tra để đảm bảo rằng trình phát không rỗng và
kích thước video không bằng VideoSize.UNKNOWN
trước khi đặt tỷ lệ
.
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)
Nếu bạn đang sử dụng trình phát tuỳ chỉnh, hãy thiết lập tỷ lệ khung hình theo chiều cao của trình phát và chiều rộng bằng cú pháp dành riêng cho trình phát của bạn. Xin lưu ý rằng nếu trình phát đổi kích thước trong quá trình khởi chạy, nếu giá trị nằm ngoài giới hạn hợp lệ của nội dung thì ứng dụng của bạn sẽ gặp sự cố. Bạn có thể cần phải thêm các bước kiểm tra khi có thể tính toán tỷ lệ khung hình, tương tự như cách tỷ lệ khung hình được thực hiện cho media3 trình phát.
Thêm thao tác từ xa
Nếu bạn muốn thêm các nút điều khiển (phát, tạm dừng, v.v.) vào cửa sổ PiP, hãy tạo một RemoteAction
cho mỗi nút điều khiển mà bạn muốn thêm.
- Thêm hằng số cho bộ điều khiển thông báo truyền tin:
// 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
- Tạo một danh sách
RemoteActions
cho các chế độ điều khiển trong cửa sổ Hình trong hình. - Tiếp theo, hãy thêm
BroadcastReceiver
và ghi đèonReceive()
để đặt giá trị hành động của từng nút. Sử dụngDisposableEffect
để đăng ký trình thu và các thao tác từ xa. Khi trình phát huỷ bỏ, hãy huỷ đăng ký người nhận.@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) } } }
- Chuyển danh sách các thao tác từ xa của bạn vào
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)
Các bước tiếp theo
Trong hướng dẫn này, bạn đã tìm hiểu các phương pháp hay nhất để thêm PiP trong Compose cả trước và sau Android 12.
- Xem ứng dụng Mạng xã hội để biết các phương pháp hay nhất về Tính năng PiP của Compose đang hoạt động.
- Hãy xem Hướng dẫn thiết kế PiP để biết thêm thông tin.