نگارش اصلی

کتابخانه media3-ui-compose مؤلفه‌های بنیادی را برای ساختن میانای کاربر رسانه در Jetpack Compose ارائه می‌دهد. این API برای توسعه‌دهندگانی طراحی شده است که به سفارشی‌سازی بیشتری نسبت به آنچه کتابخانه media3-ui-compose-material3 ارائه می‌دهد نیاز دارند. این صفحه نحوه استفاده از مؤلفه‌های اصلی و نگه‌دارنده‌های حالت را برای ایجاد میانای کاربر سفارشی پخش‌کننده رسانه توضیح می‌دهد.

ترکیب کردن Material3 و مؤلفه‌های Compose سفارشی

کتابخانه media3-ui-compose-material3 به‌گونه‌ای طراحی شده است که انعطاف‌پذیر باشد. می‌توانید از مؤلفه‌های ازپیش‌ساخته‌شده برای اکثر واسط‌های کاربر استفاده کنید، اما وقتی به کنترل بیشتری نیاز دارید، یک مؤلفه را با پیاده‌سازی سفارشی جایگزین کنید. اینجا است که کتابخانه 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 مجموعه‌ای از ترکیب‌پذیرهای ازپیش‌ساخته‌شده را برای کنترل‌های پخش‌کننده رایج ارائه می‌دهد. در اینجا برخی‌از مؤلفه‌هایی که می‌توانید مستقیماً در برنامه‌تان استفاده کنید آورده شده است:

مؤلفه شرح
PlayPauseButton محفظه وضعیت برای دکمه‌ای که بین پخش و مکث جابه‌جا می‌شود.
SeekBackButton یک محفظه وضعیت برای دکمه‌ای که با مقدار افزایشی تعریف‌شده‌ای به عقب می‌رود.
SeekForwardButton محتوی وضعیت برای دکمه‌ای که با مقدار افزایشی تعریف‌شده‌ای به‌جلو می‌رود.
NextButton محفظه وضعیت برای دکمه‌ای که به فایل رسانه‌ای بعدی می‌رود.
PreviousButton محفظه وضعیت برای دکمه‌ای که به فایل رسانه‌ای قبلی می‌رود.
RepeatButton محفظه وضعیت برای دکمه‌ای که حالت‌های تکرار را به‌صورت چرخه‌ای تغییر می‌دهد.
ShuffleButton محفظه وضعیت برای دکمه‌ای که حالت پخش درهم را روشن/خاموش می‌کند.
MuteButton محفظه وضعیت برای دکمه‌ای که پخش‌کننده را بی‌صدا و صدادار می‌کند.
TimeText یک محفظه حالت برای یک composable که پیشرفت پخش‌کننده را نمایش می‌دهد.
ContentFrame سطحی برای محتوای رسانه‌ای که مدیریت نسبت ابعادی، تغییر اندازه، شاتر، آثار هنری، و جایگاه زیرنویس را مدیریت می‌کند.
PlayerSurface Raw surface which wraps SurfaceView and TextureView in AndroidView.
Artwork یک composable که اثر هنری فایل رسانه‌ای فعلی، مانند جلد آلبوم، را نمایش می‌دهد.

سفارشی‌سازی کردن ترکیب‌پذیر Player

هنگام استفاده از کتابخانه media3-ui-compose-material3، Player composable تجربه کاملی از بازپخش رسانه ارائه می‌دهد. می‌توانید چیدمان آن را با ارائه ترکیب‌پذیرهای خود به جایگاه‌های محتوای آن سفارشی‌سازی کنید، یا می‌توانید از شیء PlayerDefaults برای سفارشی‌سازی بخش‌های خاصی از واسط کاربر، مانند TopControls، CenterControls، BottomControls، و ErrorOverlay استفاده کنید.

شکل ۱ نشان می‌دهد که ترکیب‌پذیر Player جایگاه‌هایش را کجا قرار می‌دهد. شکل ۲ ترتیب ترسیم لایه‌ها را نشان می‌دهد.

پخش‌کننده ویدیو با خط‌چین‌هایی که جایگاه‌های آن را مشخص می‌کند. از بالا به پایین: topControls در امتداد لبه بالایی، errorOverlay بالای مرکز، centerControls در وسط با دکمه‌های backSecondary،‏ back،‏ central،‏ forward، و forwardSecondary، زیرنویس‌ها نزدیک به پایین، و bottomControls در امتداد لبه پایینی با جایگاه‌های above،‏ left،‏ progressSlider،‏ right، و below. ContentFrame فضای پشت آن‌ها را پر می‌کند.
شکل ۱. جایگاه‌های تابع ترکیب‌پذیر Player. کنترل‌های وسط و پایین، جایگاه‌های PlayerDefaults.CenterControls و PlayerDefaults.BottomControls را نشان می‌دهند. ‫topControls، above، و below به‌طور پیش‌فرض خالی هستند.
نمای انفجاری از شش لایه پشته‌ای. از پایین به بالا: ۱،
      PlayerSurface؛ ۲، شاتر؛ ۳، کار هنری؛ ۴، زیرنویس؛ ۵، errorOverlay؛ و
      ۶، topControls،‏ centerControls، و bottomControls. لایه‌های ۱ تا ۴ ContentFrame را تشکیل می‌دهند و هر شش لایه Player را تشکیل می‌دهند.
شکل ۲. لایه‌های Player ترکیب‌پذیر، شماره‌گذاری‌شده به‌ترتیب ترسیم.

برای مثال، می‌توانید از PlayerDefaults.CenterControls برای لغو کردن فقط دکمه مرکزی پخش/ مکث استفاده کنید و بقیه کنترل‌های مرکزی را دست‌نخورده باقی بگذارید. همچنین می‌توانید یک ترکیب‌پذیر کاملاً سفارشی را برای پارامتری مانند 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
  )
}

Player composable همچنین زیرنویس‌ها را نمایش می‌دهد و وقتی ویدیویی برای نمایش وجود ندارد، اثر هنری فایل رسانه‌ای فعلی را نشان می‌دهد. برای سفارشی‌سازی ظاهر آن‌ها، ترکیب‌پذیرهای خودتان را به جایگاه‌های subtitles و artwork ارسال کنید. برای پنهان کردن آن‌ها، یک composable خالی را به subtitles و null را به artwork ارسال کنید.

نگهدارنده‌های حالت واسط کاربر

اگر هیچ‌کدام از مؤلفه‌های داربست نیازهای شما را برآورده نمی‌کنند، می‌توانید مستقیماً از اشیاء وضعیت استفاده کنید. به‌طورکلی توصیه می‌شود از روش‌های remember مربوطه برای حفظ ظاهر واسط کاربر بین بازترکیب‌ها استفاده کنید.

برای درک بهتر نحوه استفاده از انعطاف‌پذیری نگه‌دارنده‌های حالت میانای کاربر درمقابل ترکیب‌پذیرها، درباره نحوه مدیریت حالت توسط Compose بخوانید.

نگهدارنده‌های حالت دکمه

برای برخی‌از حالت‌های میانای کاربر، کتابخانه فرض می‌کند که به احتمال زیاد توسط ترکیب‌پذیرهای دکمه‌مانند مصرف خواهند شد.

ایالت remember*State نوع
PlayPauseButtonState rememberPlayPauseButtonState تغییر وضعیت ۲ یا تغییر وضعیت N
PreviousButtonState rememberPreviousButtonState ثابت
NextButtonState rememberNextButtonState ثابت
RepeatButtonState rememberRepeatButtonState تغییر وضعیت ۳
ShuffleButtonState rememberShuffleButtonState تغییر وضعیت ۲
PlaybackSpeedState rememberPlaybackSpeedState منو یا کلید N

علاوه‌بر وضعیت‌های پخش و مکث، PlayPauseButtonState showBuffering، showReplay، و showRetry را نشان می‌دهد. از آن‌ها برای نشان دادن نشانگر میان‌گیری درحین میان‌گیری پخش‌کننده، نماد بازپخش پس‌از پایان بازپخش، و نماد امتحان مجدد پس‌از خطای بازپخش استفاده کنید. showReplay و showRetry پالایش showPlay: هرگاه یکی از آن‌ها true باشد، showPlay نیز true است، بنابراین آن‌ها را قبل‌از showPlay بررسی کنید. روش onClick این حالت‌ها را نیز مدیریت می‌کند، برای مثال با آماده‌سازی مجدد پخش‌کننده پس‌از بروز خطا.

برای انتخاب اینکه کدام‌یک از این وضعیت‌ها نشان داده شود، ترکیبی از SHOW_BUFFERING_WHEN_PLAYING، SHOW_BUFFERING_ALWAYS، SHOW_REPLAY_ON_ENDED، و SHOW_RETRY_ON_ERROR را به پارامتر displayMode از rememberPlayPauseButtonState ارسال کنید. به‌طور پیش‌فرض، displayMode SHOW_BUFFERING_ALWAYS، SHOW_REPLAY_ON_ENDED، و SHOW_RETRY_ON_ERROR را ترکیب می‌کند، که هر سه حالت را نشان می‌دهد، و حالت میان‌گیری حتی زمانی که بازپخش موقتاً متوقف شده است ظاهر می‌شود.

نمونه استفاده از 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 تنظیم‌شده در پخش‌کننده ارائه می‌دهد. این‌ها برای نمایش اطلاعات فراداده در واسط کاربر سفارشی شما مفید هستند.

ErrorState اطلاعاتی درباره وضعیت خطای فعلی پخش‌کننده ارائه می‌دهد، که می‌توان از آن برای نمایش هم‌پوشانی خطا استفاده کرد.

PlaybackState وضعیت بازپخش اصلی پخش‌کننده را، مانند playbackState، isPlaying، و playerError، مستقل از هر مؤلفه واسط کاربر خاصی نمایش می‌دهد. از آن برای هدایت هم‌پوشانی‌ها یا چیدمان‌های سفارشی استفاده کنید، برای مثال برای نشان دادن نشانگر بارگیری درحالی‌که پخش‌کننده درحال میان‌گیری است. برای نشانگر Material3 ازپیش‌ساخته، از BufferingIndicator استفاده کنید.

PresentationState اطلاعات مربوط به زمانی را که خروجی ویدیو در PlayerSurface می‌تواند نشان داده شود یا باید با عنصر میانای کاربر جای‌بان پوشانده شود نگه می‌دارد. ContentFrame ترکیب‌پذیر، مدیریت نسبت ابعادی را با مراقبت از نمایش شاتر روی سطحی که هنوز آماده نیست ترکیب می‌کند.

@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 سیاه خواهد بود که اندازه محفظه والد را پر می‌کند.

PresentationState همچنین showArtwork را نشان می‌دهد که وقتی فایل رسانه‌ای فعلی بدون قطعه ویدیویی پخش می‌شود، مثلاً محتوای فقط صوتی، true است. در این حالت، ContentFrame ترکیب‌پذیر Artwork را نمایش می‌دهد که اثر هنری را از MediaMetadata مورد نشان می‌دهد. برای سفارشی‌سازی یا پنهان کردن کار هنری، ترکیب‌پذیر خودتان یا null را به جایگاه artwork از ContentFrame ارسال کنید.

به‌طور پیش‌فرض، Artwork فقط داده‌های اثر هنری جاسازی‌شده در فراداده یا از URI content،‏ file، یا android.resource را بار می‌کند. برای بار کردن کار هنری از URIهای راه دور یا منابع دیگر، از یکی از گزینه‌های زیر استفاده کنید:

  • ‫BitmapLoader، مثلاً DataSourceBitmapLoader، را به Artwork ارسال کنید.
  • از سربار Artwork که painter را می‌گیرد برای بار کردن تصاویر با کتابخانه انتخابی‌تان استفاده کنید.

برخلاف Player composable که به‌طور پیش‌فرض زیرنویس‌ها را ارائه می‌کند، ContentFrame جایگاه subtitles خود را خالی می‌گذارد. برای رندر کردن زیرنویس‌ها در ContentFrame، یا برای سفارشی‌سازی رندر کردن زیرنویس در Player، نشانه‌های فعلی پخش‌کننده را با CuesState مشاهده کنید و آن‌ها را در جایگاه subtitles رندر کنید. ContentFrame این جایگاه را در محدوده ویدیو مقیاس‌بندی‌شده قرار می‌دهد و آن را بالای شاتر و اثر هنری می‌کشد تا زیرنویس‌ها هنگامی که ویدیویی وجود ندارد، قابل‌مشاهده باقی بمانند.

نگهدارنده‌های حالت انتخاب قطعه

TrackSelectionState به شما کمک می‌کند منویی برای یک نوع قطعه بسازید، مثلاً منوی زبان صوتی یا زیرنویس. آن را با rememberTrackSelectionState ایجاد کنید، نوع قطعه و ترکیبی از فیلترها، مانند TRACK_FILTER_LANGUAGE یا TRACK_FILTER_RESOLUTION را ارسال کنید که تعیین می‌کنند کدام ویژگی‌های قطعه گزینه‌ها را متمایز می‌کنند. hasOffOption یا hasAutoOption را روی true تنظیم کنید تا گزینه‌ای اضافه شود که نوع قطعه را غیرفعال می‌کند یا گزینه‌ای که به پخش‌کننده اجازه می‌دهد قطعه‌ها را به‌طور خودکار انتخاب کند.

این وضعیت selectionOptions دردسترس و selectedOption را نشان می‌دهد. هر TrackSelectionOption نشان می‌دهد که آیا دستگاه از آن پشتیبانی می‌کند و آیا انتخاب شده است یا نه. ارسال گزینه‌ای به selectOption، پارامترهای انتخاب قطعه پخش‌کننده را به‌طور متناسب به‌روز می‌کند. قبل‌از نمایش منو، canSetTrackSelectionParameters و اینکه آیا گزینه‌های کافی برای انتخاب وجود دارد یا نه را بررسی کنید. برای مثال، منو زیرنویس زیر یک گزینه برای هر زبان زیرنویس و یک گزینه برای خاموش کردن زیرنویس ارائه می‌دهد:

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

اگر به دسترسی مستقیم به Tracks و TrackSelectionParameters بازیکن نیاز دارید، از TrackSelectionParametersState استفاده کنید. برای اطلاعات بیشتر درباره نحوه انتخاب قطعه توسط پخش‌کننده، به انتخاب قطعه مراجعه کنید.

‫Flows کجا است؟

بسیاری از توسعه‌دهندگان Android با استفاده از اشیاء Kotlin Flow برای جمع‌آوری داده‌های میانای کاربر که دائماً درحال تغییر است آشنا هستند. برای مثال، ممکن است به‌دنبال جریان Player.isPlaying باشید که بتوانید آن را به‌صورت آگاه از چرخه حیات collect کنید. یا چیزی مثل Player.eventsFlow تا Flow<Player.Events> را دراختیارتان قرار دهد که بتوانید آن را به روشی که می‌خواهید filter کنید.

بااین‌حال، استفاده از جریان‌ها برای وضعیت میانای کاربری Player معایبی دارد. یکی از نگرانی‌های اصلی، ماهیت ناهمگام انتقال داده است. می‌خواهیم تا حد امکان تأخیر بین Player.Event و مصرف آن در سمت میانای کاربر را کاهش دهیم و از نمایش عناصر میانای کاربر که با Player همگام‌سازی نشده‌اند جلوگیری کنیم.

نکات دیگر شامل موارد زیر است:

  • جریانی با همه Player.Events از اصل مسئولیت واحد پیروی نمی‌کند، هر مصرف‌کننده باید رویدادهای مربوطه را فیلتر کند.
  • ایجاد جریان برای هر Player.Event مستلزم این است که آن‌ها را (با combine) برای هر عنصر میانای کاربر ترکیب کنید. نگاشت چندبه‌چند بین Player.Event و تغییر عنصر میانای کاربر وجود دارد. استفاده از combine می‌تواند واسط کاربر را به وضعیت‌های احتمالاً غیرقانونی هدایت کند.

ایجاد حالت‌های واسط کاربر سفارشی

اگر حالت‌های واسط کاربر موجود نیازهایتان را برآورده نمی‌کند، می‌توانید حالت‌های واسط کاربر سفارشی اضافه کنید. کد منبع وضعیت موجود را بررسی کنید تا الگو را کپی کنید. یک کلاس نگه‌دارنده حالت رابط کاربری معمولی کارهای زیر را انجام می‌دهد:

  1. شیء Player را می‌گیرد.
  2. بااستفاده از روال‌های هم‌زمان در Player مشترک می‌شود. برای جزئیات بیشتر، به Player.listen مراجعه کنید.
  3. با به‌روزرسانی وضعیت داخلی خود به Player.Events خاصی پاسخ می‌دهد.
  4. فرمان‌های منطق کسب‌وکار را می‌پذیرد که به به‌روزرسانی مناسب Player تبدیل خواهد شد.
  5. می‌تواند در چندین مکان در سراسر درخت UI ایجاد شود و همیشه نمای ثابتی از وضعیت Player حفظ خواهد کرد.
  6. فیلدهای Compose State را که می‌توانند توسط یک 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 از حالت‌های مختلف واسط کاربر به توسعه‌دهنده نهایی کمک می‌کند تا نگران یادگیری درباره Player.Events نباشد.