هسته اصلی

کتابخانه media3-ui-compose اجزای اساسی برای ساخت رابط کاربری رسانه در Jetpack Compose را فراهم می‌کند. این کتابخانه برای توسعه‌دهندگانی طراحی شده است که به سفارشی‌سازی بیشتری نسبت به آنچه کتابخانه 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 یک کانتینر حالت برای یک کامپوننت که پیشرفت بازیکن را نمایش می‌دهد.
ContentFrame سطحی برای نمایش محتوای رسانه‌ای که مدیریت نسبت ابعاد، تغییر اندازه و شاتر را مدیریت می‌کند
PlayerSurface سطح خامی که SurfaceView و TextureView را در AndroidView پوشش می‌دهد.

سفارشی‌سازی Player قابل ترکیب

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

برای مثال، می‌توانید از PlayerDefaults.CenterControls برای لغو فقط دکمه پخش/مکث مرکزی استفاده کنید، در حالی که بقیه کنترل‌های مرکزی دست‌نخورده باقی می‌مانند. همچنین می‌توانید یک composable کاملاً سفارشی برای پارامتری مانند topControls ارسال کنید و سایر پارامترها مانند bottomControls را کاملاً حذف کنید تا آنها را به صورت پیش‌فرض خود نگه دارید.

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

دارندگان وضعیت UI

اگر هیچ یک از اجزای scaffolding نیازهای شما را برآورده نمی‌کند، می‌توانید مستقیماً از اشیاء state نیز استفاده کنید. به طور کلی توصیه می‌شود از متدهای remember مربوطه برای حفظ ظاهر رابط کاربری خود بین recompositionها استفاده کنید.

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

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

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

ایالت ایالت را به خاطر بسپار نوع
PlayPauseButtonState rememberPlayPauseButtonState ۲-تغییر وضعیت
PreviousButtonState rememberPreviousButtonState ثابت
NextButtonState rememberNextButtonState ثابت
RepeatButtonState rememberRepeatButtonState ۳-تغییر وضعیت
ShuffleButtonState rememberShuffleButtonState ۲-تغییر وضعیت
PlaybackSpeedState rememberPlaybackSpeedState منو یا N-Toggle

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

PresentationState اطلاعاتی را در مورد زمان نمایش خروجی ویدیو در PlayerSurface یا زمان پوشش آن توسط یک عنصر رابط کاربری نگه‌دارنده، نگهداری می‌کند. 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) با نسبت ابعاد انتخاب شده (برای انواع بیشتر به Content scale مراجعه کنید) و presentationState.coverSurface برای دانستن زمان مناسب برای نمایش سطح (Surface) استفاده کنیم. در این حالت، می‌توانید یک شاتر مات را روی سطح قرار دهید که وقتی سطح آماده شد، ناپدید می‌شود. ContentFrame به شما امکان می‌دهد شاتر را به عنوان یک لامبدا دنباله‌دار سفارشی کنید، اما به طور پیش‌فرض یک @Composable Box خواهد بود که اندازه کانتینر والد را پر می‌کند.

فلوز کجا هستند؟

بسیاری از توسعه‌دهندگان اندروید با استفاده از اشیاء 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. می‌تواند در چندین مکان در سراسر درخت رابط کاربری ایجاد شود و همیشه یک نمای ثابت از وضعیت بازیکن را حفظ می‌کند.
  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 نکند.