Hình trong hình (PiP) là một loại chế độ nhiều cửa sổ đặc biệt, chủ yếu dùng để phát video. API này cho phép người dùng xem video trong một cửa sổ nhỏ được ghim vào một góc của màn hình trong khi di chuyển giữa các ứng dụng hoặc duyệt qua 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 PiP vào ứng dụng, bạn cần đăng ký hoạt động, chuyển hoạt động sang chế độ PiP nếu cần, đồng thời đảm bảo ẩn các thành phần giao diện người dùng và tiếp tục phát video khi hoạt động đó ở chế độ PiP.
Hướng dẫn này mô tả cách thêm PiP trong Compose vào ứng dụng bằng cách triển khai video trong Compose. Xem ứng dụng Socialite để xem các phương pháp hay nhất này trong thực tế.
Thiết lập ứng dụng để dùng 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
rồi đặt thànhtrue
để khai báo rằng bạn sẽ sử dụng PiP trong ứng dụng. Thêm
configChanges
và đặ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. Theo đó, hoạt động của bạn sẽ không chạy lại khi các thay đổi về bố cục diễn 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 vào
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 PiP khi rời khỏi ứng dụng cho phiên bản trước Android 12
Để thêm PiP cho phiên bản trước Android 12, hãy dùng addOnUserLeaveHintProvider
. Làm theo các bước sau để thêm PiP cho phiên bản trước Android 12:
- Thêm cổng phiên bản để chỉ truy cập mã này trong 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 khionUserLeaveHintProvider
được kích hoạt bằng lambda. Trong hàm lambda, hãy gọienterPictureInPictureMode()
trênfindActivity()
rồi truyềnPictureInPictureParams.Builder().build()
. - Thêm
addOnUserLeaveHintListener
bằngfindActivity()
rồi truyền vào hàm lambda. - Trong
onDispose
, hãy thêmremoveOnUserLeaveHintListener
bằngfindActivity()
rồi 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_TAG, "API does not support PiP") }
Thêm PiP khi rời khỏi ứng dụng cho 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 truyền đến trình phát video của ứng dụng.
- Tạo
modifier
rồi gọionGloballyPositioned
trên đó. Toạ độ bố cục sẽ được sử dụng trong bước sau. - Tạo 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 rồi đặt thànhtrue
để chuyển sang chế độ PiP khi vuốt. Thao tác này sẽ tạo ảnh động mượt mà hơn so với khi truyền quaenterPictureInPictureMode
. - Sử dụng
findActivity()
để gọisetPictureInPictureParams()
. Gọibuild()
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ế độ Hình trong hình bằng cách 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 đó đến PictureInPictureParams.Builder
, vì vậy, bạn không cần phải đặt tham số mới trên trình tạo. Tuy nhiên, nếu muốn thay đổi bất kỳ tham số nào khi nhấp vào nút, bạn có thể đặt các tham số đó 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ế độ PiP (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 vào cửa sổ PiP, trừ phi bạn chỉ định giao diện người dùng sẽ hiển thị 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 đang ở chế độ Hình trong hình. Bạn có thể sử dụng OnPictureInPictureModeChangedProvider
để thực hiện việc này.
Mã dưới đây cho bạn biết ứng dụng có đang ở chế độ Hình trong hình 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ể dùng rememberIsInPipMode()
để bật/tắt các phần tử 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ế độ Hình trong hình 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 vớ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 giúp theo dõi trạng thái của trình phát video bằng mutableStateOf
.
Bật/tắt trạng thái dựa trên việc video có đang phát hay không
Để chuyển đổi 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 một 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 phát 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ó nhả tay hay không
Khi trình phát được giải phóng, hãy đặt biến trạng thái thành false
:
fun releasePlayer() { shouldEnterPipMode = false }
Dùng trạng thái để xác định xem bạn có đang dùng chế độ PiP hay không (trước Android 12)
- Vì việc thêm PiP pre-12 sẽ 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 hàm 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_TAG, "API does not support PiP") }
Dùng trạng thái để xác định xem bạn có đang dùng chế độ PiP hay không (sau Android 12)
Truyền biến trạng thái vào setAutoEnterEnabled
để ứng dụng của bạn chỉ chuyển sang 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 để chuyển sang chế độ PiP. Trên Android 12 trở lên, tính năng này cũng tạo ảnh động mượt mà hơn để thoát chế độ PiP.
Thêm API này vào trình tạo PiP để cho biết khu vực hoạt động có thể 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
nhận được từ hàmonGloballyPositioned
trên đối tượng sửa đổi. - Gọi
setSourceRectHint()
trênbuilder
rồi truyền vào biếnsourceRect
.
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)
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 cụ thể hoặc sử dụng chiều rộng và chiều cao bằng kích thước video của trình phát. Nếu bạn đang sử dụng trình phát media3, hãy kiểm tra để đảm bảo trình phát không rỗng và kích thước video của trình phát không bằng VideoSize.UNKNOWN
trước khi đặt tỷ lệ khung hình.
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 đặt tỷ lệ khung hình trên chiều cao và chiều rộng của trình phát 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 của bạn đổi kích thước trong quá trình khởi chạy, nếu trình phát nằm ngoài giới hạn hợp lệ về tỷ lệ khung hình có thể thì ứng dụng của bạn sẽ gặp sự cố. Bạn có thể cần thêm các bước kiểm tra về thời điểm có thể tính toán tỷ lệ khung hình, tương tự như cách thực hiện đối với trình phát media3.
Thêm hành động từ xa
Nếu bạn muốn thêm các chế độ điều khiển (phát, tạm dừng, v.v.) vào cửa sổ Hình trong hình, hãy tạo một RemoteAction
cho từng chế độ điều khiển mà bạn muốn thêm.
- Thêm hằng số cho các chế độ điều khiển thông báo truyền phát:
// 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 danh sách
RemoteActions
cho các chế độ điều khiển trong cửa sổ PiP. - Tiếp theo, hãy thêm
BroadcastReceiver
và ghi đèonReceive()
để đặt thao tác cho từng nút. Sử dụngDisposableEffect
để đăng ký receiver và hành động từ xa. Khi trình phát bị huỷ, hãy huỷ đăng ký trình thu.@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 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 Android 12 và sau Android 12.
- Xem ứng dụng Socialite để xem các phương pháp hay nhất về tính năng PiP trong thực tế.
- Vui lòng xem hướng dẫn thiết kế PiP để biết thêm thông tin.