شروع به کار با UI مبتنی بر نوشتن، شروع با رابط کاربری مبتنی بر نوشتن

وابستگی را اضافه کنید

کتابخانه Media3 شامل یک ماژول رابط کاربری مبتنی بر Jetpack Compose است. برای استفاده از آن، وابستگی زیر را اضافه کنید:

کاتلین

implementation("androidx.media3:media3-ui-compose:1.8.0")

گرووی

implementation "androidx.media3:media3-ui-compose:1.8.0"

ما اکیداً شما را تشویق می‌کنیم که برنامه خود را به روش Compose-first توسعه دهید یا از استفاده از Views مهاجرت کنید .

برنامه آزمایشی کاملاً آهنگسازی

اگرچه کتابخانه media3-ui-compose شامل Composableهای آماده (مانند دکمه‌ها، نشانگرها، تصاویر یا دیالوگ‌ها) نمی‌شود، اما می‌توانید یک برنامه آزمایشی (دمو) پیدا کنید که کاملاً با Compose نوشته شده باشد و از هرگونه راه‌حل قابلیت همکاری مانند قرار دادن PlayerView در AndroidView اجتناب کند. برنامه آزمایشی از کلاس‌های نگهدارنده وضعیت UI از ماژول media3-ui-compose استفاده می‌کند و از کتابخانه Compose Material3 بهره می‌برد.

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

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

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

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

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

مثالی از کاربرد PlayPauseButtonState :

@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
  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),
    )
  }
}

توجه داشته باشید که state هیچ اطلاعات قالب‌بندی مانند آیکون برای پخش یا مکث ندارد. تنها مسئولیت آن تبدیل Player به UI state است.

سپس می‌توانید دکمه‌ها را در طرح‌بندی دلخواه خود ترکیب و مطابقت دهید:

Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}

دارندگان وضعیت خروجی بصری

PresentationState اطلاعاتی را در مورد اینکه چه زمانی خروجی ویدیو در PlayerSurface می‌تواند نمایش داده شود یا باید توسط یک عنصر رابط کاربری placeholder پوشانده شود، نگه می‌دارد.

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resizeWithContentScale(ContentScale.Fit, presentationState.videoSizeDp)

Box(modifier) {
  // 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 = player,
    surfaceType = SURFACE_TYPE_SURFACE_VIEW,
    modifier = scaledModifier,
  )

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    Box(Modifier.background(Color.Black))
  }

در اینجا، می‌توانیم از presentationState.videoSizeDp برای مقیاس‌بندی سطح به نسبت ابعاد مورد نظر (برای انواع بیشتر به اسناد ContentScale مراجعه کنید) و presentationState.coverSurface برای دانستن اینکه چه زمانی زمان‌بندی برای نمایش سطح مناسب نیست، استفاده کنیم. در این حالت، می‌توانید یک شاتر مات را روی سطح قرار دهید که وقتی سطح آماده شد، ناپدید می‌شود.

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

بسیاری از توسعه‌دهندگان اندروید با استفاده از اشیاء 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(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)
      }
    }
}

برای واکنش به Player.Events خودتان، می‌توانید آنها را با استفاده از Player.listen دریافت کنید که یک suspend fun است که به شما امکان می‌دهد وارد دنیای کوروتین شوید و به طور نامحدود به Player.Events گوش دهید. پیاده‌سازی Media3 از حالت‌های مختلف رابط کاربری به توسعه‌دهنده نهایی کمک می‌کند تا خود را درگیر یادگیری در مورد Player.Events نکند.