بدء استخدام واجهة المستخدم المستندة إلى Compose

إضافة التبعية

تتضمّن مكتبة Media3 وحدة واجهة مستخدم مستندة إلى Jetpack Compose. لاستخدامها، أضِف التبعية التالية:

Kotlin

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

Groovy

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

ننصحك بشدة بتطوير تطبيقك باستخدام Compose أولاً أو نقل البيانات من استخدام Views.

تطبيق تجريبي لـ Compose بالكامل

على الرغم من أنّ مكتبة media3-ui-compose لا تتضمّن عناصر Composables جاهزة للاستخدام (مثل الأزرار أو المؤشرات أو الصور أو مربّعات الحوار)، يمكنك العثور على تطبيق تجريبي مكتوب بالكامل بلغة Compose يتجنّب أي حلول للتوافق، مثل تضمين PlayerView في AndroidView. يستخدم التطبيق التجريبي فئات حاملة لحالة واجهة المستخدم من الوحدة media3-ui-compose، كما يستفيد من مكتبة Compose Material3.

عناصر الاحتفاظ بحالة واجهة المستخدم

للتعرّف بشكل أفضل على كيفية الاستفادة من مرونة أدوات الاحتفاظ بحالة واجهة المستخدم مقارنةً بالعناصر القابلة للإنشاء، يمكنك الاطّلاع على كيفية إدارة الحالة في Compose.

عناصر الاحتفاظ بحالة الأزرار

بالنسبة إلى بعض حالات واجهة المستخدم، نفترض أنّه من المرجّح أن تستهلكها عناصر Composables تشبه الأزرار.

الولاية remember*State النوع
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState ثابت
NextButtonState rememberNextButtonState ثابت
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
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 إلى حالة واجهة المستخدم.

يمكنك بعد ذلك دمج الأزرار ومطابقتها في التنسيق الذي تفضّله:

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

عناصر الاحتفاظ بحالة الإخراج المرئي

يحتوي PresentationState على معلومات حول الوقت الذي يمكن فيه عرض ناتج الفيديو في PlayerSurface أو الوقت الذي يجب فيه تغطيته بعنصر واجهة مستخدم نائب.

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 لتغيير حجم Surface إلى نسبة العرض إلى الارتفاع المطلوبة (راجِع مستندات ContentScale لمعرفة المزيد من الأنواع) وpresentationState.coverSurface لمعرفة الوقت غير المناسب لعرض Surface. في هذه الحالة، يمكنك وضع غطاء معتم فوق السطح، وسيختفي عندما يصبح السطح جاهزًا.

أين يمكن العثور على "المهام"؟

يعرف العديد من مطوّري تطبيقات 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 في Compose التي يمكن أن تستهلكها عناصر قابلة للإنشاء من أجل الاستجابة ديناميكيًا للتغييرات.
  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.