เริ่มต้นใช้งาน UI ที่ใช้ Compose

เพิ่มการอ้างอิง

ไลบรารี Media3 มีโมดูล UI ที่อิงตาม Jetpack Compose หากต้องการใช้ ให้เพิ่ม การอ้างอิงต่อไปนี้

Kotlin

implementation("androidx.media3:media3-ui-compose:1.7.1")

Groovy

implementation "androidx.media3:media3-ui-compose:1.7.1"

เราขอแนะนำให้คุณพัฒนาแอปในลักษณะ Compose-first หรือย้ายข้อมูลจากการใช้ View

แอปเดโมที่ใช้ Compose ทั้งหมด

แม้ว่าmedia3-ui-composeไลบรารีจะไม่มี Composable ที่พร้อมใช้งาน (เช่น ปุ่ม ตัวบ่งชี้ รูปภาพ หรือกล่องโต้ตอบ) แต่คุณจะเห็นแอปเดโมที่เขียนด้วย Compose ทั้งหมดซึ่งหลีกเลี่ยงโซลูชันการทำงานร่วมกัน เช่น การห่อ PlayerView ใน AndroidView แอปเดโม ใช้คลาสที่เก็บสถานะ UI จากโมดูล media3-ui-compose และใช้ไลบรารี Compose Material3

ตัวเก็บสถานะ UI

หากต้องการทำความเข้าใจวิธีใช้ความยืดหยุ่นของที่เก็บสถานะ UI กับ Composable ให้ดียิ่งขึ้น โปรดอ่านวิธีที่ Compose จัดการสถานะ

ตัวเก็บสถานะปุ่ม

สำหรับสถานะ UI บางอย่าง เราจะถือว่าสถานะเหล่านั้นมีแนวโน้มที่จะใช้โดย Composables ที่มีลักษณะคล้ายปุ่มมากที่สุด

รัฐ remember*State ประเภท
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState ค่าคงที่
NextButtonState rememberNextButtonState ค่าคงที่
RepeatButtonState rememberRepeatButtonState 3-สลับ
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState เมนูหรือ N-Toggle

ตัวอย่างการใช้งาน PlayPauseButtonState

@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
  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),
    )
  }
}

โปรดสังเกตว่า state ไม่มีข้อมูลการจัดธีม เช่น ไอคอนที่จะใช้สำหรับการเล่น หรือหยุดชั่วคราว หน้าที่ของมันมีเพียงการเปลี่ยน Player เป็นสถานะ UI

จากนั้นคุณจะผสมผสานปุ่มต่างๆ ในเลย์เอาต์ตามที่ต้องการได้

Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}

ตัวเก็บสถานะเอาต์พุตภาพ

PresentationState มีข้อมูลเกี่ยวกับเวลาที่สามารถแสดงเอาต์พุตวิดีโอใน PlayerSurface หรือควรครอบคลุมโดยองค์ประกอบ UI ตัวยึดตำแหน่ง

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(ContentScale.Fit, presentationState.videoSizeDp)

Box(modifier) {
  // Always leave PlayerSurface to be part of the Compose tree because it will be initialised 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 = player,
    surfaceType = SURFACE_TYPE_SURFACE_VIEW,
    modifier = scaledModifier,
  )

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    Box(Modifier.background(Color.Black))
  }

ในที่นี้ เราสามารถใช้ทั้ง presentationState.videoSizeDp เพื่อปรับขนาด Surface ให้เป็นสัดส่วนภาพที่ต้องการ (ดูเอกสารContentScale สำหรับประเภทอื่นๆ) และ presentationState.coverSurface เพื่อดูว่าเมื่อใดที่เวลาไม่เหมาะสมที่จะแสดง Surface ในกรณีนี้ คุณสามารถวางชัตเตอร์ทึบแสงไว้ด้านบน ของพื้นผิว ซึ่งจะหายไปเมื่อพื้นผิวพร้อมใช้งาน

Flows อยู่ที่ไหน

นักพัฒนาแอป Android หลายคนคุ้นเคยกับการใช้ออบเจ็กต์ Kotlin Flow เพื่อรวบรวม ข้อมูล UI ที่เปลี่ยนแปลงอยู่เสมอ เช่น คุณอาจกำลังมองหาโฟลว์ Player.isPlaying ที่คุณสามารถcollect ในลักษณะที่รับรู้ถึงวงจรของคอมโพเนนต์ หรือPlayer.eventsFlowเพื่อมอบFlow<Player.Events> ให้คุณfilterได้ตามต้องการ

อย่างไรก็ตาม การใช้ Flow สำหรับสถานะ UI ของ Player มีข้อเสียบางประการ ข้อกังวลหลักอย่างหนึ่งคือลักษณะการโอนข้อมูลแบบอะซิงโครนัส เราต้องการให้มั่นใจว่าPlayer.EventและPlayerมีการตอบสนองที่รวดเร็วที่สุดเท่าที่จะเป็นไปได้ในฝั่ง UI โดยหลีกเลี่ยงการแสดงองค์ประกอบ UI ที่ไม่ซิงค์กับPlayer

ประเด็นอื่นๆ ได้แก่

  • โฟลว์ที่มี Player.Events ทั้งหมดจะไม่เป็นไปตามหลักการความรับผิดชอบเดียว ผู้บริโภคแต่ละรายจะต้องกรองเหตุการณ์ที่เกี่ยวข้องออก
  • การสร้างโฟลว์สำหรับแต่ละ Player.Event จะกำหนดให้คุณต้องรวมโฟลว์เหล่านั้น (ด้วย combine) สำหรับองค์ประกอบ UI แต่ละรายการ มีการแมปแบบหลายต่อหลายระหว่าง Player.Event กับการเปลี่ยนแปลงองค์ประกอบ UI การต้องใช้ combine อาจทำให้ UI อยู่ในสถานะที่อาจผิดกฎหมาย

สร้างสถานะ UI ที่กำหนดเอง

คุณเพิ่มสถานะ UI ที่กำหนดเองได้หากสถานะที่มีอยู่ไม่ตรงกับความต้องการ ดูซอร์สโค้ดของสถานะที่มีอยู่เพื่อคัดลอกรูปแบบ โดยทั่วไปแล้ว คลาสตัวยึดสถานะ UI จะทำสิ่งต่อไปนี้

  1. ใช้เวลา Player
  2. สมัครรับข้อมูล Player โดยใช้โครูทีน ดูรายละเอียดเพิ่มเติมได้ที่ Player.listen
  3. ตอบสนองต่อ Player.Events ที่เฉพาะเจาะจงด้วยการอัปเดตสถานะภายใน
  4. ยอมรับคำสั่งตรรกะทางธุรกิจที่จะเปลี่ยนเป็นPlayerการอัปเดตที่เหมาะสม
  5. สร้างได้หลายที่ในโครงสร้าง UI และจะรักษา มุมมองที่สอดคล้องกันของสถานะของเพลเยอร์เสมอ
  6. แสดงฟิลด์ Compose State ที่ Composable ใช้ได้เพื่อตอบสนองต่อการเปลี่ยนแปลงแบบไดนามิก
  7. มาพร้อมกับฟังก์ชัน remember*State สำหรับจดจำอินสแตนซ์ระหว่าง องค์ประกอบ

สิ่งที่เกิดขึ้นเบื้องหลัง

class SomeButtonState(private val player: Player) {
  var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_ACTION_A))
    private set

  var someField by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

  suspend fun observe() =
    player.listen { events ->
      if (
        events.containsAny(
          Player.EVENT_B_CHANGED,
          Player.EVENT_C_CHANGED,
          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
        )
      ) {
        someField = this.someField
        isEnabled = this.isCommandAvailable(Player.COMMAND_ACTION_A)
      }
    }
}

หากต้องการโต้ตอบกับ Player.Events ของคุณเอง คุณสามารถจับ Player.Events ได้โดยใช้ Player.listen ซึ่งเป็น suspend fun ที่ช่วยให้คุณเข้าสู่โลกของโครูทีนและ ฟัง Player.Events ได้อย่างไม่มีกำหนด การใช้งาน Media3 ในสถานะ UI ต่างๆ ช่วยให้นักพัฒนาแอปปลายทางไม่ต้องกังวลเรื่องการเรียนรู้เกี่ยวกับ Player.Events