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

ไลบรารี 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

ปรับแต่ง Composable Player

เมื่อใช้ไลบรารี media3-ui-compose-material3 คอมโพเนนต์ Player จะมอบประสบการณ์การเล่นสื่อที่สมบูรณ์ คุณสามารถปรับแต่งเลย์เอาต์ได้โดยระบุ Composable ของคุณเองลงใน ช่องเนื้อหา หรือใช้ประโยชน์จากออบเจ็กต์ PlayerDefaults เพื่อ ปรับแต่งบางส่วนของ UI เช่น TopControls, CenterControls, BottomControls และ ErrorOverlay

ตัวอย่างเช่น คุณสามารถใช้ PlayerDefaults.CenterControls เพื่อลบล้างเฉพาะปุ่มเล่น/หยุดชั่วคราวตรงกลางในขณะที่คงตัวควบคุมตรงกลางอื่นๆ ไว้ นอกจากนี้ คุณยังส่ง Composable ที่กำหนดเองทั้งหมดสำหรับพารามิเตอร์ เช่น topControls และละเว้นพารามิเตอร์อื่นๆ เช่น bottomControls โดยสิ้นเชิงเพื่อให้พารามิเตอร์เหล่านั้นอยู่ในค่าเริ่มต้นได้

@Composable
fun CustomPlayerSlots(player: Player, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    topControls = { p, visible ->
      // Fully custom top controls
      AnimatedVisibility(visible) { Text("My custom title") }
    },
    centerControls = { p, visible ->
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = p,
        visible = visible,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(it, modifier = Modifier.size(64.dp))
        },
      )
    },
    // bottomControls are left as default
  )
}

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

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

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

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

สำหรับสถานะ 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

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),
  )
}

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

CurrentMediaItemState ให้ข้อมูลเกี่ยวกับ MediaItem ที่กำลังเล่นอยู่ ขณะที่ PlaylistState แสดงข้อมูลเกี่ยวกับ MediaItems ที่ตั้งค่าไว้ในเครื่องเล่น ซึ่งมีประโยชน์สำหรับการแสดงข้อมูลเมตาใน UI ที่กำหนดเอง

ErrorState ให้ข้อมูลเกี่ยวกับสถานะข้อผิดพลาดปัจจุบันของเครื่องเล่น ซึ่งใช้เพื่อแสดงการวางซ้อนข้อผิดพลาดได้

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.videoAspectRatio)

  // Always leave PlayerSurface to be part of the Compose tree because it will be initialized 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.videoAspectRatio เพื่อปรับขนาดพื้นผิว ให้เป็นอัตราส่วนกว้างยาวที่เลือก (ดูประเภทเพิ่มเติมได้ที่การปรับขนาดเนื้อหา) และ presentationState.coverSurface เพื่อทราบว่าเมื่อใดที่เวลาไม่เหมาะสมที่จะ แสดงพื้นผิว ในกรณีนี้ คุณสามารถวางชัตเตอร์ทึบไว้ด้านบนของพื้นผิว ซึ่งจะหายไปเมื่อพื้นผิวพร้อม ContentFrame ช่วยให้คุณปรับแต่งชัตเตอร์เป็นแลมบ์ดาต่อท้ายได้ แต่โดยค่าเริ่มต้นจะเป็น @Composable Box สีดำที่เติมขนาดของคอนเทนเนอร์ระดับบน

Flow อยู่ที่ไหน

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

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

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

  • Flow ที่มี Player.Events ทั้งหมดจะไม่เป็นไปตามหลักการความรับผิดชอบเดียว ผู้ใช้แต่ละรายจะต้องกรองเหตุการณ์ที่เกี่ยวข้องออก
  • การสร้าง Flow สำหรับ Player.Event แต่ละรายการกำหนดให้คุณต้องรวม Flow เหล่านั้น (ด้วย 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. แสดงฟิลด์ State ของ Compose ที่ Composable ใช้เพื่อตอบสนองต่อการเปลี่ยนแปลงแบบไดนามิกได้
  7. มาพร้อมกับฟังก์ชัน remember*State สำหรับจดจำอินสแตนซ์ระหว่างการสร้าง

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

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

  var someFieldValue by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

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

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