بدء استخدام واجهة المستخدم المستندة إلى 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. تعرض هذه السمة حقول Compose State التي يمكن أن تستهلكها إحدى الوحدات القابلة للإنشاء من أجل الاستجابة بشكل ديناميكي للتغييرات.
  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.