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

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

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

Kotlin

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

Groovy

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

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

แอปเดโมที่สร้างด้วย Compose ทั้งหมด

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

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

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

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

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

รัฐ 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.resizeWithContentScale(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 ได้ตามต้องการ

อย่างไรก็ตาม การใช้โฟลว์สำหรับสถานะ 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 โดยใช้ Coroutine ดูรายละเอียดเพิ่มเติมได้ที่ 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