Chat Ajaib

Library media3-ui-compose menyediakan komponen dasar untuk membuat UI media di Jetpack Compose. API ini dirancang untuk developer yang memerlukan lebih banyak penyesuaian daripada yang ditawarkan oleh library media3-ui-compose-material3. Halaman ini menjelaskan cara menggunakan komponen inti dan holder status untuk membuat UI pemutar media kustom.

Menggabungkan komponen Material3 dan Compose kustom

Library media3-ui-compose-material3 dirancang agar fleksibel. Anda dapat menggunakan komponen bawaan untuk sebagian besar UI, tetapi menukar satu komponen dengan implementasi kustom saat Anda memerlukan kontrol lebih besar. Di sinilah library media3-ui-compose berperan.

Misalnya, bayangkan Anda ingin menggunakan PreviousButton dan NextButton standar dari library Material3, tetapi Anda memerlukan PlayPauseButton yang sepenuhnya kustom. Anda dapat melakukannya dengan menggunakan PlayPauseButton dari library media3-ui-compose inti dan menempatkannya bersama komponen bawaan.

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

Komponen yang tersedia

Library media3-ui-compose menyediakan serangkaian composable bawaan untuk kontrol pemutar umum. Berikut beberapa komponen yang dapat Anda gunakan langsung di aplikasi:

Komponen Deskripsi
PlayPauseButton Penampung status untuk tombol yang beralih antara putar dan jeda.
SeekBackButton Penampung status untuk tombol yang memutar mundur dengan penambahan yang ditentukan.
SeekForwardButton Penampung status untuk tombol yang memajukan video dengan penambahan yang ditentukan.
NextButton Penampung status untuk tombol yang mencari item media berikutnya.
PreviousButton Penampung status untuk tombol yang mencari item media sebelumnya.
RepeatButton Penampung status untuk tombol yang beralih di antara mode ulang.
ShuffleButton Penampung status untuk tombol yang mengaktifkan/menonaktifkan mode acak.
MuteButton Penampung status untuk tombol yang membisukan dan membunyikan audio pemutar.
TimeText Penampung status untuk composable yang menampilkan progres pemain.
ContentFrame Platform untuk menampilkan konten media yang menangani pengelolaan rasio aspek, pengubahan ukuran, dan shutter
PlayerSurface Permukaan mentah yang menggabungkan SurfaceView dan TextureView di AndroidView.

Menyesuaikan composable Player

Saat menggunakan library media3-ui-compose-material3, composable Player memberikan pengalaman pemutaran media yang lengkap. Anda dapat menyesuaikan tata letaknya dengan menyediakan composable Anda sendiri ke slot kontennya, atau Anda dapat memanfaatkan objek PlayerDefaults untuk menyesuaikan bagian tertentu dari UI, seperti TopControls, CenterControls, BottomControls, dan ErrorOverlay.

Gambar 1 menunjukkan tempat composable Player menempatkan slotnya. Gambar 2 menunjukkan urutan penggambaran lapisannya.

Pemutar video dengan garis batas putus-putus yang menandai slotnya. Dari atas ke
      bawah: topControls di sepanjang tepi atas, errorOverlay di atas tengah,
      centerControls di tengah dengan tombol backSecondary, back, central,
      forward, dan forwardSecondary, subtitel di dekat bagian bawah, dan
      bottomControls di sepanjang tepi bawah dengan slot di atas, kiri,
      progressSlider, kanan, dan di bawah. ContentFrame mengisi area
      di belakangnya.
Gambar 1. Slot composable Player. Kontrol tengah dan bawah menampilkan slot PlayerDefaults.CenterControls dan PlayerDefaults.BottomControls. Slot topControls, above, dan below kosong secara default.
Tampilan terurai dari enam lapisan bertumpuk. Dari bawah ke atas: 1,
      PlayerSurface; 2, shutter; 3, artwork; 4, subtitel; 5, errorOverlay; dan
      6, topControls, centerControls, dan bottomControls. Lapisan 1 hingga 4 membentuk
      ContentFrame, dan keenam lapisan tersebut membentuk Player.
Gambar 2. Lapisan composable Player, diberi nomor sesuai urutan penggambarannya.

Misalnya, Anda dapat menggunakan PlayerDefaults.CenterControls untuk mengganti tombol putar/jeda tengah saja, sekaligus membiarkan kontrol tengah lainnya tetap utuh. Anda juga dapat meneruskan composable yang sepenuhnya kustom untuk parameter seperti topControls, dan sepenuhnya menghilangkan parameter lain seperti bottomControls untuk membiarkannya dalam setelan default.

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

Holder status UI

Jika tidak ada komponen scaffolding yang memenuhi kebutuhan, Anda juga dapat menggunakan objek status secara langsung. Sebaiknya gunakan metode remember yang sesuai untuk mempertahankan tampilan UI Anda di antara rekomposisi.

Untuk lebih memahami cara menggunakan fleksibilitas holder status UI versus composable, baca cara Compose mengelola Status.

Holder status tombol

Untuk beberapa status UI, library membuat asumsi bahwa status tersebut kemungkinan besar akan digunakan oleh composable seperti tombol.

Negara Bagian remember*State Jenis
PlayPauseButtonState rememberPlayPauseButtonState 2-Tombol
PreviousButtonState rememberPreviousButtonState Konstanta
NextButtonState rememberNextButtonState Konstanta
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Tombol
PlaybackSpeedState rememberPlaybackSpeedState Menu atau N-Toggle

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

Holder status output visual

CurrentMediaItemState memberikan informasi tentang MediaItem yang sedang diputar, sedangkan PlaylistState mengekspos informasi tentang MediaItems yang ditetapkan pada pemutar. Hal ini berguna untuk menampilkan informasi metadata di UI kustom Anda.

ErrorState memberikan informasi tentang status error pemutar saat ini, yang dapat digunakan untuk menampilkan overlay error.

PresentationState menyimpan informasi kapan output video dalam PlayerSurface dapat ditampilkan atau harus dicakup oleh elemen UI placeholder. Composable ContentFrame menggabungkan penanganan rasio aspek dengan menampilkan shutter di atas platform yang belum siap.

@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()
  }
}

Di sini, kita dapat menggunakan presentationState.videoAspectRatio untuk menskalakan Surface ke rasio aspek yang dipilih (lihat Skala konten untuk jenis lainnya) dan presentationState.coverSurface untuk mengetahui kapan waktu yang tidak tepat untuk menampilkan Surface. Dalam hal ini, Anda dapat menempatkan shutter buram di atas permukaan, yang akan hilang saat permukaan siap. ContentFrame memungkinkan Anda menyesuaikan shutter sebagai lambda di akhir, tetapi secara default, shutter akan berupa @Composable Box hitam yang mengisi ukuran penampung induk.

Di mana Flow?

Banyak developer Android yang terbiasa menggunakan objek Flow Kotlin untuk mengumpulkan data UI yang terus berubah. Misalnya, Anda mungkin mencari flow Player.isPlaying yang dapat Anda collect dengan cara yang mendukung siklus proses. Atau sesuatu seperti Player.eventsFlow untuk memberi Anda Flow<Player.Events> yang dapat Anda filter sesuai keinginan Anda.

Namun, menggunakan flow untuk status UI Player memiliki beberapa kelemahan. Salah satu masalah utamanya adalah sifat transfer data yang asinkron. Kita ingin mencapai latensi sekecil mungkin antara Player.Event dan konsumsinya di sisi UI, sehingga menghindari menampilkan elemen UI yang tidak sinkron dengan Player.

Poin lainnya meliputi:

  • Alur dengan semua Player.Events tidak akan mematuhi prinsip tanggung jawab tunggal, setiap konsumen harus memfilter peristiwa yang relevan.
  • Membuat flow untuk setiap Player.Event akan mengharuskan Anda menggabungkannya (dengan combine) untuk setiap elemen UI. Ada pemetaan many-to-many antara Player.Event dan perubahan elemen UI. Penggunaan combine dapat menyebabkan UI berada dalam status yang berpotensi ilegal.

Membuat status UI kustom

Anda dapat menambahkan status UI kustom jika status yang ada tidak memenuhi kebutuhan Anda. Lihat kode sumber status yang ada untuk menyalin polanya. Class holder status UI standar melakukan hal berikut:

  1. Mengambil objek Player.
  2. Berlangganan ke Player menggunakan coroutine. Lihat Player.listen untuk mengetahui detail selengkapnya.
  3. Merespons Player.Events tertentu dengan memperbarui status internalnya.
  4. Menerima perintah logika bisnis yang akan diubah menjadi update Player yang sesuai.
  5. Dapat dibuat di beberapa tempat di seluruh hierarki UI dan akan selalu mempertahankan tampilan status Pemutar yang konsisten.
  6. Mengekspos kolom State Compose yang dapat digunakan oleh composable untuk merespons perubahan secara dinamis.
  7. Dilengkapi dengan fungsi remember*State untuk mengingat instance di antara komposisi.

Yang terjadi di balik layar:

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

Untuk bereaksi terhadap Player.Events Anda sendiri, Anda dapat menangkapnya menggunakan Player.listen yang merupakan suspend fun yang memungkinkan Anda memasuki dunia coroutine dan memproses Player.Events tanpa batas waktu. Implementasi Media3 dari berbagai status UI membantu developer akhir agar tidak perlu mempelajari Player.Events.