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

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

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

Kotlin

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

رائع

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

ننصحك بشدة بتطوير تطبيقك باستخدام ميزة "الإنشاء" أولاً أو التوقف عن استخدام "العروض".

تطبيق تجريبي لميزة "الإنشاء بالكامل"

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

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

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

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

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

الولاية remember*الحالة النوع
PlayPauseButtonState rememberPlayPauseButtonState 2-تبديل
PreviousButtonState rememberPreviousButtonState ثابت
NextButtonState rememberNextButtonState ثابت
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-تبديل
PlaybackSpeedState rememberPlaybackSpeedState القائمة أو التبديل المتعدد

مثال على استخدام 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.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 لمعرفة الحالات التي لا يكون فيها التوقيت مناسبًا ل عرض المساحة. في هذه الحالة، يمكنك وضع غالق معتم فوق سطح التصوير، والذي سيختفي عندما يصبح السطح جاهزًا.

أين يمكنني العثور على "عمليات التنقّل"؟

يُعرف العديد من مطوّري تطبيقات Android باستخدام عناصر Flow في Kotlin لجمع بيانات واجهة المستخدم المتغيّرة باستمرار. على سبيل المثال، قد تبحث عن تدفق Player.isPlaying يمكنك collect بطريقة تراعي دورة الحياة. أو شيء مثل Player.eventsFlow لتزويدك بـ Flow<Player.Events> يمكنك filter بالطريقة التي تريدها.

ومع ذلك، فإنّ استخدام مسارات الحالة Player لواجهة المستخدم له بعض السلبيات. ومن بين الملاحظات العميقة التي يجب أخذها في الاعتبار، الطبيعة غير المتزامنة لنقل البيانات. نريد التأكّد من خفض وقت الاستجابة إلى الحد الأدنى بين Player.Event واستهلاكه من جهة ملف Player، مع تجنُّب عرض عناصر واجهة المستخدم غير المتزامنة مع 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 ل reagting بشكل ديناميكي إلى التغييرات.
  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.