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

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

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

کاتلین

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

شیار

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

ما شدیداً شما را تشویق می‌کنیم که برنامه‌تان را به شیوه «اول نوشتن» توسعه دهید یا از استفاده از Views خارج شوید .

برنامه نمایشی کاملاً نوشتن

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

دارندگان ایالت UI

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

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

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

ایالت به یاد داشته باشید * دولت تایپ کنید
PlayPauseButtonState rememberPlayPauseButtonState 2-تغییر
PreviousButtonState rememberPreviousButtonState ثابت
NextButtonState rememberNextButtonState ثابت
RepeatButtonState rememberRepeatButtonState 3-تغییر
ShuffleButtonState rememberShuffleButtonState 2-تغییر
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 است.

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

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

دارندگان حالت خروجی بصری

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

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(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 برای جمع آوری داده های UI همیشه در حال تغییر آشنا هستند. برای مثال، ممکن است به دنبال جریان Player.isPlaying باشید که می‌توانید آن را به شیوه‌ای آگاه از چرخه حیات collect . یا چیزی مانند Player.eventsFlow تا یک Flow<Player.Events> را در اختیار شما قرار دهد که بتوانید به روشی که می خواهید filter .

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

سایر نکات عبارتند از:

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

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

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

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

،

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

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

کاتلین

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

شیار

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

ما شدیداً شما را تشویق می‌کنیم که برنامه‌تان را به شیوه «اول نوشتن» توسعه دهید یا از استفاده از Views خارج شوید .

برنامه نمایشی کاملاً نوشتن

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

دارندگان ایالت UI

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

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

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

ایالت به یاد داشته باشید * دولت تایپ کنید
PlayPauseButtonState rememberPlayPauseButtonState 2-تغییر
PreviousButtonState rememberPreviousButtonState ثابت
NextButtonState rememberNextButtonState ثابت
RepeatButtonState rememberRepeatButtonState 3-تغییر
ShuffleButtonState rememberShuffleButtonState 2-تغییر
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 است.

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

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

دارندگان حالت خروجی بصری

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

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(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 برای جمع آوری داده های UI همیشه در حال تغییر آشنا هستند. برای مثال، ممکن است به دنبال جریان Player.isPlaying باشید که می‌توانید آن را به شیوه‌ای آگاه از چرخه حیات collect . یا چیزی مانند Player.eventsFlow تا یک Flow<Player.Events> را در اختیار شما قرار دهد که بتوانید به روشی که می خواهید filter .

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

سایر نکات عبارتند از:

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

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

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

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