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

ไลบรารี 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 พื้นผิวแบบ Raw ที่รวม SurfaceView และ TextureView ไว้ใน AndroidView

ปรับแต่ง Composable Player

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

รูปที่ 1 แสดงตำแหน่งที่คอมโพสิเบิล Player วางช่องของตน รูปที่ 2 แสดงลำดับการวาดเลเยอร์

วิดีโอเพลเยอร์ที่มีเส้นประแสดงช่องโฆษณา จากบนลงล่าง: topControls ตามขอบด้านบน, errorOverlay เหนือตรงกลาง, centerControls ตรงกลางพร้อมปุ่ม backSecondary, back, central, forward และ forwardSecondary, คำบรรยายใกล้ด้านล่าง และ bottomControls ตามขอบด้านล่างพร้อมช่อง above, left, progressSlider, right และ below ContentFrame จะเติมเต็มพื้นที่ด้านหลัง
รูปที่ 1 ช่องของฟังก์ชันที่ประกอบกันได้ Player ตัวควบคุมตรงกลางและด้านล่างจะแสดงช่องของ PlayerDefaults.CenterControls และ PlayerDefaults.BottomControls topControls, above และ below จะว่างเปล่าโดยค่าเริ่มต้น
มุมมองแบบแยกส่วนของเลเยอร์ 6 ชั้นที่ซ้อนกัน จากล่างขึ้นบน: 1, PlayerSurface; 2, shutter; 3, artwork; 4, subtitles; 5, errorOverlay; และ 6, topControls, centerControls และ bottomControls เลเยอร์ 1 ถึง 4 ประกอบกันเป็น ContentFrame และทั้ง 6 เลเยอร์ประกอบกันเป็น Player
รูปที่ 2 เลเยอร์ของ Player Composable ซึ่งมีหมายเลขกำกับตามลำดับที่วาด

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

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

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

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

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

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

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

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

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

Flows อยู่ที่ไหน

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

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