Chat Ajaib

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

Menggabungkan komponen Compose kustom dan Material 3

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 yang 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 Anda:

Komponen Deskripsi
PlayPauseButton Penampung status untuk tombol yang beralih antara putar dan jeda.
SeekBackButton Penampung status untuk tombol yang mencari mundur dengan inkrement yang ditentukan.
SeekForwardButton Penampung status untuk tombol yang mencari ke depan dengan inkrement 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 bergantian melalui mode pengulangan.
ShuffleButton Penampung status untuk tombol yang mengalihkan mode acak.
MuteButton Penampung status untuk tombol yang membisukan dan mengaktifkan suara pemutar.
TimeText Container status untuk composable yang menampilkan progres pemain.
ContentFrame Platform untuk menampilkan konten media yang menangani pengelolaan rasio aspek, pengubahan ukuran, dan rana
PlayerSurface Permukaan mentah yang membungkus SurfaceView dan TextureView di AndroidView.

Holder status UI

Jika tidak ada komponen scaffolding yang memenuhi kebutuhan Anda, 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 mengasumsikan bahwa status tersebut kemungkinan besar akan digunakan oleh Composable seperti tombol.

Status 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

PresentationState menyimpan informasi tentang kapan output video di PlayerSurface dapat ditampilkan atau harus dicakup oleh elemen UI placeholder. Composable ContentFrame menggabungkan penanganan rasio aspek dengan menangani penampilan tombol rana di atas permukaan 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.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()
  }
}

Di sini, kita dapat menggunakan kedua presentationState.videoSizeDp untuk menskalakan Platform ke rasio aspek yang dipilih (lihat dokumen ContentScale untuk jenis lainnya) dan presentationState.coverSurface untuk mengetahui kapan waktu yang tepat untuk menampilkan Platform. Dalam hal ini, Anda dapat memosisikan penutup buram di atas permukaan, yang akan menghilang saat permukaan siap. ContentFrame memungkinkan Anda menyesuaikan rana sebagai lambda di akhir, tetapi secara default, rana akan berupa @Composable Box hitam yang mengisi ukuran penampung induk.

Di mana letak Flow?

Banyak developer Android yang terbiasa menggunakan objek Flow Kotlin untuk mengumpulkan data UI yang terus berubah. Misalnya, Anda mungkin mencari alur 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 alur untuk status UI Player memiliki beberapa kekurangan. Salah satu masalah utama adalah sifat transfer data yang asinkron. Kita ingin mencapai latensi sekecil mungkin antara Player.Event dan konsumsinya di sisi UI, dengan menghindari menampilkan elemen UI yang tidak sinkron dengan Player.

Poin lainnya mencakup:

  • 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 pola. Class holder status UI standar melakukan hal berikut:

  1. Menerima 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 pembaruan Player yang sesuai.
  5. Dapat dibuat di beberapa tempat di seluruh hierarki UI dan akan selalu mempertahankan tampilan status Pemain 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(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)
      }
    }
}

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. Penerapan Media3 dari berbagai status UI membantu developer akhir tidak perlu mempelajari Player.Events.