Core Compose

media3-ui-compose kitaplığı, Jetpack Compose'da medya kullanıcı arayüzü oluşturmak için temel bileşenleri sağlar. media3-ui-compose-material3 kitaplığının sunduğundan daha fazla özelleştirmeye ihtiyaç duyan geliştiriciler için tasarlanmıştır. Bu sayfada, özel bir medya oynatıcı kullanıcı arayüzü oluşturmak için temel bileşenlerin ve durum bilgisi depolayıcıların nasıl kullanılacağı açıklanmaktadır.

Material3 ve özel Compose bileşenlerini karıştırma

media3-ui-compose-material3 kitaplığı esnek olacak şekilde tasarlanmıştır. Kullanıcı arayüzünüzün çoğu için önceden oluşturulmuş bileşenleri kullanabilir, daha fazla kontrole ihtiyaç duyduğunuzda tek bir bileşeni özel bir uygulamayla değiştirebilirsiniz. Bu noktada media3-ui-compose kitaplığı devreye girer.

Örneğin, Material3 kitaplığındaki standart PreviousButton ve NextButton bileşenlerini kullanmak istediğinizi ancak tamamen özel bir PlayPauseButton bileşenine ihtiyacınız olduğunu varsayalım. Bunu, temel media3-ui-compose kitaplığındaki PlayPauseButton bileşenini kullanarak ve önceden oluşturulmuş bileşenlerin yanına yerleştirerek yapabilirsiniz.

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

Kullanılabilir bileşenler

media3-ui-compose kitaplığı, yaygın oynatıcı kontrolleri için önceden oluşturulmuş bir dizi composable sağlar. Doğrudan uygulamanızda kullanabileceğiniz bileşenlerden bazıları şunlardır:

Bileşen Açıklama
PlayPauseButton Oynatma ve duraklatma arasında geçiş yapan bir düğmenin durum kapsayıcısı.
SeekBackButton Tanımlanmış bir artışla geri sarma düğmesi için durum kapsayıcısı.
SeekForwardButton Belirli bir artışla ileri saran bir düğmenin durum kapsayıcısı.
NextButton Sonraki medya öğesine saran bir düğme için durum kapsayıcısı.
PreviousButton Önceki medya öğesine saran bir düğmenin durum kapsayıcısı.
RepeatButton Tekrar modları arasında geçiş yapan bir düğme için durum kapsayıcısı.
ShuffleButton Karışık modunu açıp kapatan bir düğme için durum kapsayıcısı.
MuteButton Oynatıcının sesini kapatıp açan bir düğme için durum kapsayıcısı.
TimeText Oynatıcı ilerleme durumunu gösteren bir composable için durum kapsayıcısı.
ContentFrame En boy oranı yönetimi, yeniden boyutlandırma, deklanşör, poster ve altyazı alanını işleyen medya içeriği yüzeyi.
PlayerSurface AndroidView içinde SurfaceView ve TextureView'i sarmalayan ham yüzey.
Artwork Mevcut medya öğesinin resmini (ör. albüm kapağı) gösteren bir composable.

Player composable'ını özelleştirme

media3-ui-compose-material3 kitaplığını kullanırken Player composable'ı, eksiksiz bir medya oynatma deneyimi sunar. İçerik alanlarına kendi composable'larınızı ekleyerek düzenini özelleştirebilir veya PlayerDefaults nesnesini kullanarak kullanıcı arayüzünün TopControls, CenterControls, BottomControls ve ErrorOverlay gibi belirli bölümlerini özelleştirebilirsiniz.

Şekil 1'de, Player composable'ının yuvalarını nereye yerleştirdiği gösterilmektedir. Şekil 2'de, katmanların çizilme sırası gösterilmektedir.

Yuvalarını işaretleyen kesikli ana hatlara sahip bir video oynatıcı. Yukarıdan aşağıya: Üst kenarda topControls, ortanın üstünde errorOverlay, ortada backSecondary, back, central, forward ve forwardSecondary düğmeleriyle centerControls, alt kısma yakın altyazılar ve alt kenarda above, left, progressSlider, right ve below yuvalarıyla bottomControls. ContentFrame, bu öğelerin arkasındaki alanı doldurur.
Şekil 1. Player composable'ının alanları. Ortadaki ve alttaki kontroller, PlayerDefaults.CenterControls ve PlayerDefaults.BottomControls için ayrılan alanları gösterir. topControls, above ve below yuvaları varsayılan olarak boştur.
Üst üste yerleştirilmiş altı katmanın patlatılmış görünümü. Aşağıdan yukarıya: 1, PlayerSurface; 2, shutter; 3, artwork; 4, subtitles; 5, errorOverlay; ve 6, topControls, centerControls ve bottomControls. 1-4. katmanlar ContentFrame'i, altı katmanın tamamı ise Player'ı oluşturur.
Şekil 2. Player composable'ının katmanları, çizildikleri sıraya göre numaralandırılır.

Örneğin, PlayerDefaults.CenterControls kullanarak yalnızca ortadaki oynat/duraklat düğmesini geçersiz kılabilir ve diğer orta kontrolleri olduğu gibi bırakabilirsiniz. Ayrıca, topControls gibi bir parametre için tamamen özel bir composable iletebilir ve bottomControls gibi diğerlerini tamamen atlayarak varsayılan değerlerinde bırakabilirsiniz.

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

Player composable bileşeni, altyazıları ve gösterilecek video olmadığında mevcut medya öğesinin posterini de gösterir. Görünümlerini özelleştirmek için kendi composable'larınızı subtitles ve artwork alanlarına iletin. Bunları gizlemek için subtitles'e boş bir composable, artwork'ye ise null iletin.

Kullanıcı arayüzü durum bilgisi depolayıcıları

İskelet bileşenlerinden hiçbiri ihtiyaçlarınızı karşılamıyorsa durum nesnelerini doğrudan da kullanabilirsiniz. Yeniden oluşturma işlemleri arasında kullanıcı arayüzü görünümünüzü korumak için genellikle ilgili remember yöntemlerini kullanmanız önerilir.

Kullanıcı arayüzü durum bilgisi depolayıcıların ve composable'ların esnekliğinden nasıl yararlanabileceğinizi daha iyi anlamak için Compose'un durumu nasıl yönettiği hakkında bilgi edinin.

Düğme durum bilgisi depolayıcıları

Bazı kullanıcı arayüzü durumları için kitaplık, bunların büyük olasılıkla düğme benzeri composable'lar tarafından kullanılacağını varsayar.

Eyalet remember*State Tür
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle veya N-Toggle
PreviousButtonState rememberPreviousButtonState Sabit
NextButtonState rememberNextButtonState Sabit
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState Menü veya N-Toggle

Oynatma ve duraklatma durumlarına ek olarak PlayPauseButtonState; showBuffering, showReplay ve showRetry durumlarını da gösterir. Oynatıcı arabelleğe alırken arabelleğe alma göstergesi, oynatma bittikten sonra tekrar oynatma simgesi ve oynatma hatasından sonra yeniden deneme simgesi göstermek için bunları kullanın. showReplay ve showRetry, showPlay'yi daraltır: Bunlardan biri true olduğunda showPlay de true olur. Bu nedenle, showPlay'den önce bunları kontrol edin. onClick yöntemi bu durumları da işler. Örneğin, bir hatadan sonra oynatıcıyı tekrar hazırlar.

Bu durumlardan hangilerinin gösterileceğini seçmek için SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED ve SHOW_RETRY_ON_ERROR kombinasyonunu rememberPlayPauseButtonState öğesinin displayMode parametresine iletin. Varsayılan olarak displayMode, SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED ve SHOW_RETRY_ON_ERROR durumlarını birleştirir. Bu üç durumun tümü gösterilir ve oynatma duraklatıldığında bile arabelleğe alma durumu görünür.

PlayPauseButtonState için örnek kullanım:

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

Görsel çıkış durum bilgisi depolayıcıları

CurrentMediaItemState, oynatılmakta olan MediaItem hakkında bilgi verirken PlaylistState, oynatıcıda ayarlanan MediaItems hakkında bilgi verir. Bunlar, özel kullanıcı arayüzünüzde yayınlanan içerik meta verisi bilgilerini göstermek için kullanışlıdır.

ErrorState, oynatıcının mevcut hata durumu hakkında bilgi sağlar. Bu bilgi, hata yer paylaşımı göstermek için kullanılabilir.

PlaybackState, belirli bir kullanıcı arayüzü bileşeninden bağımsız olarak oynatıcının temel oynatma durumunu (ör. playbackState, isPlaying ve playerError) gösterir. Bu bileşeni, yer paylaşımlarını veya özel düzenleri yönlendirmek için kullanabilirsiniz. Örneğin, oynatıcı arabelleğe alma işlemi yaparken yükleme göstergesi gösterebilirsiniz. Önceden oluşturulmuş bir Material3 göstergesi için BufferingIndicator kullanın.

PresentationState, PlayerSurface içindeki video çıkışının ne zaman gösterilebileceği veya yer tutucu kullanıcı arayüzü öğesiyle ne zaman kapatılması gerektiğiyle ilgili bilgileri içerir. ContentFrame composable, en boy oranı işlemeyi henüz hazır olmayan bir yüzey üzerinde deklanşörün gösterilmesiyle birleştirir.

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

Burada, Surface'ı seçilen en boy oranına ölçeklendirmek için presentationState.videoAspectRatio (daha fazla tür için İçerik ölçeği bölümüne bakın) ve Surface'ı göstermek için doğru zamanın ne zaman olmadığını öğrenmek için presentationState.coverSurface kullanabiliriz. Bu durumda, yüzeyin üzerine opak bir deklanşör yerleştirebilirsiniz. Yüzey hazır olduğunda bu deklanşör kaybolur. ContentFrame, deklanşörü lambda olarak özelleştirmenize olanak tanır ancak varsayılan olarak üst kapsayıcının boyutunu dolduran siyah bir @Composable Box olur.

PresentationState, geçerli medya öğesi video parçası olmadan (ör. yalnızca ses içeren içerik) oynatıldığında true olan showArtwork değerini de gösterir. Bu durumda ContentFrame, öğenin MediaMetadata öğesindeki posteri gösteren Artwork composable'ını gösterir. Albüm kapağını özelleştirmek veya gizlemek için kendi composable'ınızı ya da null öğesini ContentFrame öğesinin artwork alanına iletin.

Varsayılan olarak Artwork, yalnızca meta verilere yerleştirilmiş albüm kapağı verilerini veya bir content, file ya da android.resource URI'sinden gelen verileri yükler. Albüm kapaklarını uzak URI'lerden veya diğer kaynaklardan yüklemek için aşağıdaki seçeneklerden birini kullanın:

  • Artwork için DataSourceBitmapLoader gibi bir BitmapLoader iletin.
  • Seçtiğiniz kitaplıkla resim yüklemek için painter alan Artwork aşırı yüklemesini kullanın.

Altyazıları varsayılan olarak oluşturan Player bileşeninin aksine, ContentFrame, subtitles alanını boş bırakır. Altyazıları ContentFrame içinde oluşturmak veya Player içinde altyazı oluşturmayı özelleştirmek için oynatıcının mevcut işaretlerini CuesState ile gözlemleyin ve bunları subtitles alanında oluşturun. ContentFrame, bu alanı ölçeklendirilmiş videonun sınırları içinde düzenler ve deklanşör ile posterin üzerine çizer. Böylece video olmadığında altyazılar görünür kalır.

Parça seçim durumu bilgisi depolayıcıları

TrackSelectionState, tek bir parça türü (ör. ses dili veya altyazı menüsü) için menü oluşturmanıza yardımcı olur. Kanal türünü ve seçenekleri ayırt eden kanal özelliklerini belirleyen filtrelerin (ör. TRACK_FILTER_LANGUAGE veya TRACK_FILTER_RESOLUTION) bir kombinasyonunu ileterek rememberTrackSelectionState ile oluşturun. Parça türünü devre dışı bırakan veya oynatıcının parçaları otomatik olarak seçmesine olanak tanıyan bir seçenek eklemek için hasOffOption veya hasAutoOption değerini true olarak ayarlayın.

Durum, kullanılabilir selectionOptions ve selectedOption değerlerini gösterir. Her TrackSelectionOption, cihazın bu özelliği destekleyip desteklemediğini ve seçilip seçilmediğini gösterir. selectOption yöntemine bir seçenek iletildiğinde oynatıcının parça seçim parametreleri buna göre güncellenir. Menüyü göstermeden önce canSetTrackSelectionParameters ve yeterli seçenek olup olmadığını kontrol edin. Örneğin, aşağıdaki altyazı menüsünde her altyazı dili için bir seçenek ve altyazıları kapatma seçeneği sunulur:

@Composable
fun SubtitleMenu(player: Player, modifier: Modifier = Modifier) {
  val state =
    rememberTrackSelectionState(
      player,
      trackType = C.TRACK_TYPE_TEXT,
      trackFilters = TRACK_FILTER_LANGUAGE,
      hasOffOption = true,
    )
  // Hide the menu if the player can't change tracks or has no subtitles to choose from
  if (!state.canSetTrackSelectionParameters || state.selectionOptions.size < 2) return

  Column(modifier.selectableGroup()) {
    for (option in state.selectionOptions) {
      Row(
        Modifier.fillMaxWidth()
          .heightIn(min = 48.dp)
          .selectable(
            selected = option.isSelected,
            enabled = option.isSupported,
            role = Role.RadioButton,
            onClick = { state.selectOption(option) },
          )
          .padding(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalAlignment = Alignment.CenterVertically,
      ) {
        RadioButton(selected = option.isSelected, onClick = null, enabled = option.isSupported)
        Text(
          text =
            if (option.onOffState == TRACK_SELECTION_OFF) "Off"
            else option.language?.let { Locale.forLanguageTag(it).displayName } ?: "Unknown",
          color =
            if (option.isSupported) Color.Unspecified
            else LocalContentColor.current.copy(alpha = 0.38f),
        )
      }
    }
  }
}

Oyuncunun Tracks ve TrackSelectionParameters bilgilerine doğrudan erişmeniz gerekiyorsa bunun yerine TrackSelectionParametersState kullanın. Oynatıcının parçaları nasıl seçtiği hakkında daha fazla bilgi için Parça seçimi bölümüne bakın.

Flows nerede?

Birçok Android geliştirici, sürekli değişen kullanıcı arayüzü verilerini toplamak için Kotlin Flow nesnelerini kullanmaya aşinadır. Örneğin, yaşam döngüsüne duyarlı bir şekilde collect yapabileceğiniz bir Player.isPlaying akışı arıyor olabilirsiniz. Ya da istediğiniz gibi filter Flow<Player.Events> sunmak için Player.eventsFlow gibi bir şey.

Ancak Player kullanıcı arayüzü durumu için akışları kullanmanın bazı dezavantajları vardır. En önemli endişelerden biri, veri aktarımının eşzamansız olmasıdır. Player.Event ile kullanıcı arayüzü tarafında tüketimi arasında mümkün olduğunca az gecikme olmasını ve Player ile senkronize olmayan kullanıcı arayüzü öğelerinin gösterilmesini önlemek istiyoruz.

Diğer noktalar şunlardır:

  • Tüm Player.Events öğelerini içeren bir akış, tek sorumluluk ilkesine uymaz. Her tüketicinin ilgili etkinlikleri filtrelemesi gerekir.
  • Her Player.Event için bir akış oluşturmak, her kullanıcı arayüzü öğesi için bunları birleştirmenizi (combine ile) gerektirir. Player.Event ile kullanıcı arayüzü öğesi değişikliği arasında çoktan çoğa eşleme vardır. combine kullanmak zorunda kalmak, kullanıcı arayüzünü potansiyel olarak yasa dışı durumlara yönlendirebilir.

Özel kullanıcı arayüzü durumları oluşturma

Mevcut kullanıcı arayüzü durumları ihtiyaçlarınızı karşılamıyorsa özel kullanıcı arayüzü durumları ekleyebilirsiniz. Kalıbı kopyalamak için mevcut durumun kaynak koduna göz atın. Tipik bir kullanıcı arayüzü durum bilgisi depolayıcı sınıfı şunları yapar:

  1. Player nesnesi alır.
  2. Eş yordamları kullanarak Player akışına abone olur. Daha fazla bilgi için Player.listen sayfasına bakın.
  3. Dahili durumunu güncelleyerek belirli Player.Events yanıt verir.
  4. Uygun bir Player güncellemesine dönüştürülecek iş mantığı komutlarını kabul eder.
  5. Kullanıcı arayüzü ağacında birden fazla yerde oluşturulabilir ve her zaman oynatıcının durumunu tutarlı bir şekilde gösterir.
  6. Değişikliklere dinamik olarak yanıt vermek için bir composable tarafından kullanılabilecek Compose State alanlarını gösterir.
  7. Oluşturmalar arasında örneği hatırlamak için remember*State işleviyle birlikte gelir.

Arka planda gerçekleşen işlemler:

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

Kendi Player.Events akışınıza tepki vermek için Player.listen kullanarak akışı yakalayabilirsiniz. Bu, eş yordam dünyasına girmenize ve Player.Events akışını süresiz olarak dinlemenize olanak tanıyan bir suspend fun işlevidir. Media3'ün çeşitli kullanıcı arayüzü durumlarını uygulaması, son geliştiricinin Player.Events hakkında bilgi edinme konusunda endişelenmemesine yardımcı olur.