نگارش اصلی

کتابخانه 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 composable تجربه کاملی از پخش رسانه ارائه می‌دهد. می‌توانید چیدمان آن را با ارائه عناصر ترکیبی خودتان به جایگاه‌های محتوای آن سفارشی‌سازی کنید، یا می‌توانید از شیء PlayerDefaults برای سفارشی‌سازی بخش‌های خاصی از رابط کاربری، مانند TopControls، CenterControls، BottomControls، و ErrorOverlay استفاده کنید.

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

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

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

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

‫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. می‌تواند در مکان‌های مختلف در درخت واسط کاربر ایجاد شود و همیشه نمای ثابتی از وضعیت «بازیکن» را حفظ می‌کند.
  6. فیلدهای «نوشتن» State را آشکار می‌کند که می‌توانند توسط یک عنصر ترکیبی مصرف شوند تا به‌صورت پویا به تغییرات پاسخ دهند.
  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 نباشد.