ไลบรารี 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 |
พื้นผิวแบบ Raw ที่รวม SurfaceView และ TextureView ไว้ใน AndroidView |
ปรับแต่ง Composable Player
เมื่อใช้ไลบรารี media3-ui-compose-material3 คอมโพสิเบิล Player จะมอบประสบการณ์การเล่นสื่อที่สมบูรณ์แบบ คุณสามารถปรับแต่งเลย์เอาต์ได้โดยระบุ Composable ของคุณเองลงในช่องเนื้อหา หรือจะใช้ออบเจ็กต์ PlayerDefaults เพื่อปรับแต่งส่วนที่เฉพาะเจาะจงของ UI เช่น TopControls, CenterControls, BottomControls และ ErrorOverlay ก็ได้
รูปที่ 1 แสดงตำแหน่งที่คอมโพสิเบิล Player วางช่องของตน รูปที่ 2 แสดงลำดับการวาดเลเยอร์
Player
ตัวควบคุมตรงกลางและด้านล่างจะแสดงช่องของ PlayerDefaults.CenterControls และ PlayerDefaults.BottomControls topControls,
above และ below จะว่างเปล่าโดยค่าเริ่มต้นPlayer Composable ซึ่งมีหมายเลขกำกับตามลำดับที่วาดเช่น คุณสามารถใช้ PlayerDefaults.CenterControls เพื่อลบล้างเฉพาะปุ่มเล่น/หยุดชั่วคราวตรงกลาง โดยปล่อยให้การควบคุมตรงกลางที่เหลือเหมือนเดิม
นอกจากนี้ คุณยังส่ง Composable ที่กำหนดเองทั้งหมดสำหรับพารามิเตอร์ เช่น topControls และละเว้นพารามิเตอร์อื่นๆ เช่น bottomControls เพื่อปล่อยให้เป็นค่าเริ่มต้นได้ด้วย
@Composable fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) { Player( player = player, modifier = modifier, showControls = showControls, topControls = { // Fully custom top controls Text("My custom title") }, centerControls = { // Use default CenterControls but override the central button PlayerDefaults.CenterControls( player = player, central = { // A custom play/pause button Material3PlayPauseButton(player, modifier = Modifier.size(64.dp)) }, ) }, // bottomControls are left as default ) }
ตัวเก็บสถานะ UI
หากคอมโพเนนต์โครงสร้างพื้นฐานไม่ตรงกับความต้องการ คุณยังใช้ออบเจ็กต์สถานะได้โดยตรง โดยทั่วไป เราขอแนะนำให้ใช้เมธอด remember ที่เกี่ยวข้องเพื่อรักษารูปลักษณ์ของ UI ระหว่างการจัดองค์ประกอบใหม่
หากต้องการทำความเข้าใจให้ดียิ่งขึ้นเกี่ยวกับวิธีใช้ความยืดหยุ่นของตัวเก็บสถานะ UI เทียบกับ Composable โปรดอ่านวิธีที่ Compose จัดการสถานะ
ตัวเก็บสถานะของปุ่ม
สำหรับสถานะ UI บางสถานะ ไลบรารีจะถือว่าสถานะเหล่านั้นมีแนวโน้มที่จะใช้โดยคอมโพสิเบิลที่คล้ายปุ่ม
| รัฐ | remember*State | ประเภท |
|---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
ค่าคงที่ |
NextButtonState |
rememberNextButtonState |
ค่าคงที่ |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
PlaybackSpeedState |
rememberPlaybackSpeedState |
เมนูหรือปุ่มสลับ N |
ตัวอย่างการใช้ 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 เพื่อปรับขนาด Surface ให้เป็นสัดส่วนการแสดงผลที่เลือก (ดูประเภทเพิ่มเติมได้ที่การปรับขนาดเนื้อหา) และ presentationState.coverSurface เพื่อให้ทราบว่าเมื่อใดที่ไม่เหมาะสมที่จะแสดง Surface ในกรณีนี้ คุณสามารถวางชัตเตอร์ทึบแสงไว้ด้านบนของพื้นผิว ซึ่งจะหายไปเมื่อพื้นผิวพร้อมใช้งาน
ContentFrame ช่วยให้คุณปรับแต่งชัตเตอร์เป็นแลมบ์ดาต่อท้ายได้ แต่โดยค่าเริ่มต้นจะเป็น @Composable Box สีดำที่เติมขนาดของคอนเทนเนอร์หลัก
Flows อยู่ที่ไหน
นักพัฒนาแอป Android หลายคนคุ้นเคยกับการใช้ออบเจ็กต์ Kotlin Flow เพื่อรวบรวมข้อมูล UI ที่เปลี่ยนแปลงตลอดเวลา เช่น คุณอาจมองหาขั้นตอน Player.isPlaying ที่สามารถcollectในลักษณะที่คำนึงถึงวงจรได้ หรืออะไรทำนองนั้นPlayer.eventsFlowเพื่อมอบFlow<Player.Events>ที่คุณสามารถfilterได้ตามต้องการ
อย่างไรก็ตาม การใช้โฟลว์สำหรับสถานะ UI ของ Player มีข้อเสียบางประการ ข้อกังวลหลักประการหนึ่งคือลักษณะแบบอะซิงโครนัสของการโอนข้อมูล เราต้องการให้มีเวลาในการตอบสนองน้อยที่สุดระหว่าง Player.Event และการใช้งานในฝั่ง UI โดยหลีกเลี่ยงการแสดงองค์ประกอบ UI ที่ไม่สอดคล้องกับ Player
ประเด็นอื่นๆ มีดังนี้
- โฟลว์ที่มี
Player.Eventsทั้งหมดจะไม่เป็นไปตามหลักการความรับผิดชอบเดียว ผู้ใช้แต่ละรายจะต้องกรองเหตุการณ์ที่เกี่ยวข้องออก - การสร้างโฟลว์สำหรับ
Player.Eventแต่ละรายการจะกำหนดให้คุณต้องรวมโฟลว์เหล่านั้นเข้าด้วยกัน (ด้วยcombine) สำหรับองค์ประกอบ UI แต่ละรายการ มีการแมปแบบหลายต่อหลายระหว่าง Player.Event กับการเปลี่ยนแปลงองค์ประกอบ UI การต้องใช้combineอาจทำให้ UI อยู่ในสถานะที่อาจผิดกฎหมาย
สร้างสถานะ UI ที่กำหนดเอง
คุณสามารถเพิ่มสถานะ UI ที่กำหนดเองได้หากสถานะที่มีอยู่ไม่ตรงกับความต้องการ ดูซอร์สโค้ดของสถานะที่มีอยู่เพื่อคัดลอกรูปแบบ คลาสตัวเก็บสถานะ UI ทั่วไปจะทำสิ่งต่อไปนี้
- รับออบเจ็กต์
Player - สมัครรับข้อมูล
Playerโดยใช้โครูทีน ดูรายละเอียดเพิ่มเติมได้ที่Player.listen - ตอบสนองต่อ
Player.Eventsที่เฉพาะเจาะจงโดยการอัปเดตสถานะภายใน - ยอมรับคำสั่งตรรกะทางธุรกิจที่จะเปลี่ยนเป็นการอัปเดต
Playerที่เหมาะสม - สามารถสร้างได้ในหลายตำแหน่งในโครงสร้าง UI และจะรักษามุมมองที่สอดคล้องกันของสถานะ Player เสมอ
- แสดงช่อง
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 ที่ช่วยให้คุณเข้าสู่โลกของโครูทีนและ
ฟัง Player.Events ได้อย่างไม่มีกำหนด การใช้งาน Media3 ของสถานะ UI ต่างๆ ช่วยให้นักพัฒนาแอปปลายทางไม่ต้องกังวลเกี่ยวกับการเรียนรู้เกี่ยวกับ Player.Events