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

เพิ่มการพึ่งพา

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

Kotlin

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

Groovy

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

เราขอแนะนําอย่างยิ่งให้คุณพัฒนาแอปในลักษณะ Compose ก่อน หรือย้ายข้อมูลจากการใช้ Views

แอปเดโมที่คอมโพสิทอย่างเต็มรูปแบบ

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

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

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

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

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

รัฐ remember*State ประเภท
PlayPauseButtonState rememberPlayPauseButtonState 2-สลับ
PreviousButtonState rememberPreviousButtonState ค่าคงที่
NextButtonState rememberNextButtonState ค่าคงที่
RepeatButtonState rememberRepeatButtonState สวิตช์โยก 3 ทาง
ShuffleButtonState rememberShuffleButtonState 2-สลับ
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 คือเปลี่ยน 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 ในกรณีนี้ คุณสามารถวางชัตเตอร์ทึบไว้ด้านบนของพื้นผิว ซึ่งจะหายไปเมื่อพื้นผิวพร้อมใช้งาน

ฟีดอยู่ที่ไหน

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

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

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

  • โฟลว์ที่มี Player.Events ทั้งหมดจะไม่เป็นไปตามหลักการความรับผิดชอบแบบรวมศูนย์ ผู้บริโภคแต่ละรายจะต้องกรองเหตุการณ์ที่เกี่ยวข้องออก
  • การสร้างโฟลว์สำหรับ Player.Event แต่ละรายการจะทำให้คุณต้องรวม 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 ที่ช่วยให้คุณเข้าสู่โลกของ coroutine และฟัง Player.Events ได้โดยไม่มีกำหนด การใช้งาน UI ต่างๆ ของ Media3 ช่วยให้นักพัฒนาแอปไม่ต้องกังวลเรื่องการเรียนรู้เกี่ยวกับPlayer.Events