เริ่มต้นใช้งาน 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-Toggle
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 ตัวยึดตำแหน่ง 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.videoSizeDp)

  // 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, scaledModifier, surfaceType)

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    shutter()
  }
}

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

Flows อยู่ที่ไหน

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

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