ฟีเจอร์ช่วยเขียน

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 พื้นผิวดิบที่ห่อหุ้ม SurfaceView และ TextureView ใน AndroidView

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

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

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

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

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

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

ตัวอย่างการใช้งาน 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),
  )
}

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

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กับการใช้งานในฝั่ง 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