ไลบรารี media3-ui-compose มีคอมโพเนนต์พื้นฐานสำหรับการสร้าง UI ของสื่อใน Jetpack Compose โดยออกแบบมาสำหรับนักพัฒนาแอปที่ต้องการการปรับแต่งมากกว่าที่ไลบรารี media3-ui-compose-material3 มีให้ หน้านี้จะอธิบายวิธีใช้คอมโพเนนต์หลักและตัวเก็บสถานะเพื่อสร้าง UI ของเครื่องเล่นสื่อที่กำหนดเอง
การผสมผสานคอมโพเนนต์ Material3 และคอมโพเนนต์ Compose ที่กำหนดเอง
ไลบรารี media3-ui-compose-material3 ได้รับการออกแบบมาให้มีความยืดหยุ่น คุณสามารถใช้คอมโพเนนต์ที่สร้างไว้ล่วงหน้าสำหรับ UI ส่วนใหญ่ แต่สลับคอมโพเนนต์เดียวเป็นการใช้งานที่กำหนดเองเมื่อต้องการควบคุมมากขึ้น ซึ่งเป็นจุดที่ไลบรารี media3-ui-compose เข้ามามีบทบาท
ตัวอย่างเช่น สมมติว่าคุณต้องการใช้มาตรฐาน
PreviousButton และ NextButton จาก
ไลบรารี Material3 แต่ต้องการ PlayPauseButton ที่กำหนดเองทั้งหมด คุณทำได้โดยใช้ PlayPauseButton จากไลบรารีหลัก media3-ui-compose
และวางไว้ข้างคอมโพเนนต์ที่สร้างไว้ล่วงหน้า
Row { // Use prebuilt component from the Media3 UI Compose Material3 library PreviousButton(player) // Use the scaffold component from Media3 UI Compose library PlayPauseButton(player) { // `this` is PlayPauseButtonState FilledTonalButton( onClick = { Log.d("PlayPauseButton", "Clicking on play-pause button") this.onClick() }, enabled = this.isEnabled, ) { Icon( imageVector = if (showPlay) Icons.Default.PlayArrow else Icons.Default.Pause, contentDescription = if (showPlay) "Play" else "Pause", ) } } // Use prebuilt component from the Media3 UI Compose Material3 library NextButton(player) }
คอมโพเนนต์ที่พร้อมใช้งาน
ไลบรารี media3-ui-compose มี Composable ที่สร้างไว้ล่วงหน้าสำหรับตัวควบคุมเครื่องเล่นทั่วไป ต่อไปนี้คือคอมโพเนนต์บางส่วนที่คุณใช้ในแอปได้โดยตรง
| คอมโพเนนต์ | คำอธิบาย |
|---|---|
PlayPauseButton |
คอนเทนเนอร์สถานะสำหรับปุ่มที่สลับระหว่างเล่นและหยุดชั่วคราว |
SeekBackButton |
คอนเทนเนอร์สถานะสำหรับปุ่มที่กรอถอยหลังตามส่วนเพิ่มที่กำหนด |
SeekForwardButton |
คอนเทนเนอร์สถานะสำหรับปุ่มที่กรอไปข้างหน้าตามส่วนเพิ่มที่กำหนด |
NextButton |
คอนเทนเนอร์สถานะสำหรับปุ่มที่กรอไปยังรายการสื่อถัดไป |
PreviousButton |
คอนเทนเนอร์สถานะสำหรับปุ่มที่กรอไปยังรายการสื่อก่อนหน้า |
RepeatButton |
คอนเทนเนอร์สถานะสำหรับปุ่มที่วนรอบโหมดเล่นซ้ำ |
ShuffleButton |
คอนเทนเนอร์สถานะสำหรับปุ่มที่สลับโหมดสับเปลี่ยน |
MuteButton |
คอนเทนเนอร์สถานะสำหรับปุ่มที่ปิดและเปิดเสียงของเครื่องเล่น |
TimeText |
คอนเทนเนอร์สถานะสำหรับ Composable ที่แสดงความคืบหน้าของเครื่องเล่น |
ContentFrame |
พื้นผิวสำหรับแสดงเนื้อหาสื่อที่จัดการการจัดการอัตราส่วนกว้างยาว การปรับขนาด และชัตเตอร์ |
PlayerSurface |
พื้นผิวแบบดิบที่ห่อหุ้ม SurfaceView และ TextureView ใน AndroidView |
ปรับแต่ง Composable Player
เมื่อใช้ไลบรารี media3-ui-compose-material3 คอมโพเนนต์
Player จะมอบประสบการณ์การเล่นสื่อที่สมบูรณ์
คุณสามารถปรับแต่งเลย์เอาต์ได้โดยระบุ Composable ของคุณเองลงใน
ช่องเนื้อหา หรือใช้ประโยชน์จากออบเจ็กต์ PlayerDefaults เพื่อ
ปรับแต่งบางส่วนของ UI เช่น TopControls, CenterControls,
BottomControls และ ErrorOverlay
ตัวอย่างเช่น คุณสามารถใช้ PlayerDefaults.CenterControls เพื่อลบล้างเฉพาะปุ่มเล่น/หยุดชั่วคราวตรงกลางในขณะที่คงตัวควบคุมตรงกลางอื่นๆ ไว้
นอกจากนี้ คุณยังส่ง Composable ที่กำหนดเองทั้งหมดสำหรับพารามิเตอร์ เช่น topControls และละเว้นพารามิเตอร์อื่นๆ เช่น bottomControls โดยสิ้นเชิงเพื่อให้พารามิเตอร์เหล่านั้นอยู่ในค่าเริ่มต้นได้
@Composable fun CustomPlayerSlots(player: Player, modifier: Modifier = Modifier) { Player( player = player, modifier = modifier, topControls = { p, visible -> // Fully custom top controls AnimatedVisibility(visible) { Text("My custom title") } }, centerControls = { p, visible -> // Use default CenterControls but override the central button PlayerDefaults.CenterControls( player = p, visible = visible, central = { // A custom play/pause button Material3PlayPauseButton(it, modifier = Modifier.size(64.dp)) }, ) }, // bottomControls are left as default ) }
ตัวเก็บสถานะ UI
หากไม่มีคอมโพเนนต์โครงสร้างใดตรงกับความต้องการ คุณก็สามารถใช้ออบเจ็กต์สถานะได้โดยตรง โดยทั่วไปแล้ว เราขอแนะนำให้ใช้วิธีการ remember ที่เกี่ยวข้องเพื่อรักษาลักษณะ UI ไว้ระหว่างการสร้างใหม่
อ่านเกี่ยวกับวิธีที่ Compose จัดการสถานะเพื่อทำความเข้าใจวิธีใช้ความยืดหยุ่นของตัวเก็บสถานะ UI กับ Composable ได้ดียิ่งขึ้น
ตัวเก็บสถานะปุ่ม
สำหรับสถานะ UI บางสถานะ ไลบรารีจะสันนิษฐานว่าสถานะเหล่านั้นมีแนวโน้มที่จะใช้โดย Composable ที่คล้ายปุ่ม
| สถานะ | remember*State | ประเภท |
|---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
ค่าคงที่ |
NextButtonState |
rememberNextButtonState |
ค่าคงที่ |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
PlaybackSpeedState |
rememberPlaybackSpeedState |
เมนูหรือ N-Toggle |
ตัวอย่างการใช้ PlayPauseButtonState
val state = rememberPlayPauseButtonState(player) IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) { Icon( imageVector = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause, contentDescription = if (state.showPlay) stringResource(R.string.playpause_button_play) else stringResource(R.string.playpause_button_pause), ) }
ตัวเก็บสถานะเอาต์พุตภาพ
CurrentMediaItemState ให้ข้อมูลเกี่ยวกับ
MediaItem ที่กำลังเล่นอยู่ ขณะที่ PlaylistState แสดงข้อมูลเกี่ยวกับ MediaItems
ที่ตั้งค่าไว้ในเครื่องเล่น ซึ่งมีประโยชน์สำหรับการแสดงข้อมูลเมตาใน UI ที่กำหนดเอง
ErrorState ให้ข้อมูลเกี่ยวกับสถานะข้อผิดพลาดปัจจุบันของเครื่องเล่น ซึ่งใช้เพื่อแสดงการวางซ้อนข้อผิดพลาดได้
PresentationState เก็บข้อมูลสำหรับกรณีที่เอาต์พุตวิดีโอใน
PlayerSurface แสดงได้หรือควรครอบคลุมโดยองค์ประกอบ UI ตัวยึดตำแหน่ง
ContentFrame composable ผสานการจัดการอัตราส่วนกว้างยาวกับการดูแล
การแสดงชัตเตอร์เหนือพื้นผิวที่ยังไม่พร้อม
@Composable fun ContentFrame( player: Player?, modifier: Modifier = Modifier, surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW, contentScale: ContentScale = ContentScale.Fit, keepContentOnReset: Boolean = false, shutter: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(Color.Black)) }, ) { val presentationState = rememberPresentationState(player, keepContentOnReset) val scaledModifier = modifier.resizeWithContentScale(contentScale, presentationState.videoAspectRatio) // Always leave PlayerSurface to be part of the Compose tree because it will be initialized in // the process. If this composable is guarded by some condition, it might never become visible // because the Player won't emit the relevant event, e.g. the first frame being ready. PlayerSurface(player, scaledModifier, surfaceType) if (presentationState.coverSurface) { // Cover the surface that is being prepared with a shutter shutter() } }
ในที่นี้ เราสามารถใช้ทั้ง presentationState.videoAspectRatio เพื่อปรับขนาดพื้นผิว
ให้เป็นอัตราส่วนกว้างยาวที่เลือก (ดูประเภทเพิ่มเติมได้ที่การปรับขนาดเนื้อหา) และ
presentationState.coverSurface เพื่อทราบว่าเมื่อใดที่เวลาไม่เหมาะสมที่จะ
แสดงพื้นผิว ในกรณีนี้ คุณสามารถวางชัตเตอร์ทึบไว้ด้านบนของพื้นผิว ซึ่งจะหายไปเมื่อพื้นผิวพร้อม
ContentFrame ช่วยให้คุณปรับแต่งชัตเตอร์เป็นแลมบ์ดาต่อท้ายได้ แต่โดยค่าเริ่มต้นจะเป็น @Composable Box สีดำที่เติมขนาดของคอนเทนเนอร์ระดับบน
Flow อยู่ที่ไหน
นักพัฒนาแอป Android หลายคนคุ้นเคยกับการใช้ออบเจ็กต์ Flow ของ Kotlin เพื่อรวบรวมข้อมูล UI ที่เปลี่ยนแปลงตลอดเวลา ตัวอย่างเช่น คุณอาจกำลังมองหา Flow Player.isPlaying ที่คุณสามารถ collect ในลักษณะที่รับรู้ถึงวงจรการทำงาน หรือ
อย่างเช่น Player.eventsFlow เพื่อให้ Flow<Player.Events>
ที่คุณสามารถ filter ได้ตามต้องการ
อย่างไรก็ตาม การใช้ Flow สำหรับสถานะ UI ของ Player มีข้อเสียบางประการ ข้อกังวลหลักอย่างหนึ่งคือลักษณะการทำงานแบบไม่พร้อมกันของการโอนข้อมูล เราต้องการให้เกิดความหน่วงน้อยที่สุดเท่าที่จะเป็นไปได้ระหว่าง Player.Event กับการใช้ Player.Event ในฝั่ง
UI เพื่อหลีกเลี่ยงการแสดงองค์ประกอบ UI ที่ไม่ซิงค์กับ Player
ประเด็นอื่นๆ ได้แก่
- Flow ที่มี
Player.Eventsทั้งหมดจะไม่เป็นไปตามหลักการความรับผิดชอบเดียว ผู้ใช้แต่ละรายจะต้องกรองเหตุการณ์ที่เกี่ยวข้องออก - การสร้าง Flow สำหรับ
Player.Eventแต่ละรายการกำหนดให้คุณต้องรวม Flow เหล่านั้น (ด้วยcombine) สำหรับองค์ประกอบ UI แต่ละรายการ มีการแมปแบบหลายต่อหลายระหว่าง Player.Event กับการเปลี่ยนแปลงองค์ประกอบ UI การต้องใช้combineอาจทำให้ UI อยู่ในสถานะที่อาจไม่ถูกต้อง
สร้างสถานะ UI ที่กำหนดเอง
คุณสามารถเพิ่มสถานะ UI ที่กำหนดเองได้หากสถานะที่มีอยู่ไม่ตรงกับความต้องการ ดูซอร์สโค้ดของสถานะที่มีอยู่เพื่อคัดลอกรูปแบบ คลาสตัวเก็บสถานะ UI ทั่วไปจะทำสิ่งต่อไปนี้
- รับออบเจ็กต์
Player - ติดตาม
Playerโดยใช้ Coroutine ดูรายละเอียดเพิ่มเติมได้ที่Player.listen - ตอบสนองต่อ
Player.Eventsที่เฉพาะเจาะจงโดยการอัปเดตสถานะภายใน - ยอมรับคำสั่งตรรกะทางธุรกิจที่จะเปลี่ยนเป็นข้อมูลอัปเดต
Playerที่เหมาะสม - สร้างได้หลายตำแหน่งในโครงสร้าง UI และจะรักษาข้อมูลสถานะของเครื่องเล่นที่สอดคล้องกันเสมอ
- แสดงฟิลด์
Stateของ Compose ที่ Composable ใช้เพื่อตอบสนองต่อการเปลี่ยนแปลงแบบไดนามิกได้ - มาพร้อมกับฟังก์ชัน
remember*Stateสำหรับจดจำอินสแตนซ์ระหว่างการสร้าง
สิ่งที่เกิดขึ้นเบื้องหลัง
class SomeButtonState(private val player: Player) { var isEnabled by mutableStateOf(player.isCommandAvailable(COMMAND_ACTION_A)) private set var someFieldValue by mutableStateOf(someFieldDefault) private set fun onClick() { player.actionA() } suspend fun observe(): Nothing = player.listen { events -> if (events.containsAny(EVENT_B_CHANGED, EVENT_C_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { someFieldValue = this.someField isEnabled = this.isCommandAvailable(COMMAND_ACTION_A) } } }
หากต้องการตอบสนองต่อ Player.Events ของคุณเอง คุณสามารถดักจับเหตุการณ์เหล่านั้นได้โดยใช้ Player.listen
ซึ่งเป็น suspend fun ที่ช่วยให้คุณเข้าสู่โลกของ Coroutine และ
ติดตาม Player.Events ได้อย่างไม่จำกัด การใช้งานสถานะ UI ต่างๆ ของ Media3 ช่วยให้นักพัฒนาแอปไม่ต้องกังวลกับการเรียนรู้เกี่ยวกับ Player.Events