Core Compose

توفّر مكتبة media3-ui-compose المكوّنات الأساسية لإنشاء واجهة مستخدم للوسائط في Jetpack Compose. وهي مصمَّمة للمطوّرين الذين يحتاجون إلى مزيد من خيارات التخصيص مقارنةً بما تقدّمه مكتبة media3-ui-compose-material3. توضّح هذه الصفحة كيفية استخدام المكوّنات الأساسية وعناصر الاحتفاظ بالحالة لإنشاء واجهة مستخدم مخصّصة لمشغّل الوسائط.

الجمع بين Material3 ومكوّنات Compose المخصّصة

تم تصميم مكتبة media3-ui-compose-material3 لتكون مرنة. يمكنك استخدام المكوّنات المُعدّة مسبقًا لمعظم واجهة المستخدم، ولكن يمكنك استبدال مكوّن واحد لتنفيذ مخصّص عندما تحتاج إلى مزيد من التحكّم. هنا يأتي دور مكتبة media3-ui-compose.

على سبيل المثال، لنفترض أنّك تريد استخدام PreviousButton وNextButton العاديَين من مكتبة Material3، ولكنّك تحتاج إلى PlayPauseButton مخصّص بالكامل. يمكنك تحقيق ذلك باستخدام PlayPauseButton من مكتبة media3-ui-compose الأساسية ووضعها بجانب المكوّنات المُنشأة مسبقًا.

Row {
  // Use prebuilt component from the Media3 UI Compose Material3 library
  PreviousButton(player)
  // Use the scaffold component from Media3 UI Compose library
  PlayPauseButton(player) {
    // `this` is PlayPauseButtonState
    FilledTonalButton(
      onClick = {
        Log.d("PlayPauseButton", "Clicking on play-pause button")
        this.onClick()
      },
      enabled = this.isEnabled,
    ) {
      Icon(
        imageVector = if (showPlay) Icons.Default.PlayArrow else Icons.Default.Pause,
        contentDescription = if (showPlay) "Play" else "Pause",
      )
    }
  }
  // Use prebuilt component from the Media3 UI Compose Material3 library
  NextButton(player)
}

المكوّنات المتاحة

توفّر مكتبة media3-ui-compose مجموعة من الدوال المركّبة المُعدّة مسبقًا لعناصر التحكم في المشغّل الشائعة. فيما يلي بعض المكونات التي يمكنك استخدامها مباشرة في تطبيقك:

المكوّن الوصف
PlayPauseButton حاوية حالة لزرّ يبدّل بين التشغيل والإيقاف المؤقت
SeekBackButton حاوية حالة لزر يقدّم المحتوى إلى الخلف بمقدار محدّد
SeekForwardButton حاوية حالة لزر يقدّم الفيديو بمقدار محدّد
NextButton حاوية حالة لزرّ يقدّم إلى ملف الوسائط التالي
PreviousButton حاوية حالة لزرّ يرجع إلى ملف الوسائط السابق
RepeatButton حاوية حالة لزر يتنقّل بين أوضاع التكرار
ShuffleButton حاوية حالة لزرّ يتيح تفعيل وضع الترتيب العشوائي أو إيقافه
MuteButton حاوية حالة لزر يكتم صوت المشغّل ويعيد صوته
TimeText حاوية حالة لدالة مركّبة تعرض مستوى تقدّم المشغّل
ContentFrame مساحة لعرض محتوى الوسائط التي تتعامل مع إدارة نسبة العرض إلى الارتفاع وتغيير الحجم والغالق
PlayerSurface سطح أولي يلتف حول SurfaceView وTextureView في AndroidView.

تخصيص الدالة المركّبة Player

عند استخدام مكتبة media3-ui-compose-material3، توفّر الدالة البرمجية المركّبة Player تجربة تشغيل وسائط كاملة. يمكنك تخصيص تنسيقها من خلال توفير الدوال المركّبة الخاصة بك لخانات المحتوى، أو يمكنك الاستفادة من الكائن PlayerDefaults لتخصيص أجزاء معيّنة من واجهة المستخدم، مثل TopControls وCenterControls وBottomControls وErrorOverlay.

يوضّح الشكل 1 مكان وضع الدالة المركّبة Player لخاناتها. يوضح الشكل 2 الترتيب الذي يرسم به طبقاته.

مشغّل فيديو مع خطوط خارجية متقطّعة تحدّد خاناته من الأعلى إلى الأسفل: عناصر التحكّم العلوية على طول الحافة العلوية، وتراكب الخطأ فوق الوسط، وعناصر التحكّم المركزية في الوسط مع أزرار الرجوع الثانوي والرجوع والوسط والتقديم والتقديم الثانوي، والترجمة بالقرب من الأسفل، وعناصر التحكّم السفلية على طول الحافة السفلية مع الخانات العلوية واليسرى وشريط تمرير مستوى التقدم واليمنى والسفلية. ويملأ ContentFrame المساحة
      خلفهما.
الشكل 1. خانات الدالة المركّبة Player تعرض عناصر التحكّم في الوسط والأسفل خانات PlayerDefaults.CenterControls و PlayerDefaults.BottomControls. تكون الخانات topControls وabove وbelow فارغة تلقائيًا.
عرض مفصّل لست طبقات مكدّسة من الأسفل إلى الأعلى: 1،
      PlayerSurface، و2، الغالق، و3، العمل الفني، و4، الترجمة، و5، errorOverlay، و6،
      topControls وcenterControls وbottomControls. تتألف ContentFrame من الطبقات من 1 إلى 4، بينما يتألف المشغّل من الطبقات الست كلها.
الشكل 2. طبقات الدالة المركّبة Player، مرقّمة حسب ترتيب رسمها

على سبيل المثال، يمكنك استخدام PlayerDefaults.CenterControls لإلغاء زر التشغيل/الإيقاف المؤقت المركزي فقط مع ترك بقية عناصر التحكّم المركزية كما هي. يمكنك أيضًا تمرير دالة مركّبة مخصّصة بالكامل لمعلمة مثل topControls، وحذف معلمات أخرى تمامًا مثل bottomControls لتركها على الإعدادات التلقائية.

@Composable
fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    showControls = showControls,
    topControls = {
      // Fully custom top controls
      Text("My custom title")
    },
    centerControls = {
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = player,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(player, modifier = Modifier.size(64.dp))
        },
      )
    },
    // bottomControls are left as default
  )
}

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

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

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

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

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

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

مثال على استخدام PlayPauseButtonState:

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),
  )
}

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

يوفّر CurrentMediaItemState معلومات حول MediaItem الذي يتم تشغيله حاليًا، بينما يعرض PlaylistState معلومات حول MediaItems الذي تم ضبطه على المشغّل. وهي مفيدة لعرض معلومات البيانات الوصفية في واجهة المستخدم المخصّصة.

ErrorState يوفّر معلومات عن حالة الخطأ الحالية للمشغّل، ويمكن استخدامها لعرض تراكب خطأ.

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

@Composable
fun ContentFrame(
  player: Player?,
  modifier: Modifier = Modifier,
  surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW,
  contentScale: ContentScale = ContentScale.Fit,
  keepContentOnReset: Boolean = false,
  shutter: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(Color.Black)) },
) {
  val presentationState = rememberPresentationState(player, keepContentOnReset)
  val scaledModifier =
    modifier.resizeWithContentScale(contentScale, presentationState.videoAspectRatio)

  // Always leave PlayerSurface to be part of the Compose tree because it will be initialized 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, scaledModifier, surfaceType)

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    shutter()
  }
}

هنا، يمكننا استخدام كل من presentationState.videoAspectRatio لتغيير حجم Surface إلى نسبة العرض إلى الارتفاع المحددة (انظر Content scale لمزيد من الأنواع) و presentationState.coverSurface لمعرفة متى يكون التوقيت غير مناسب لعرض Surface. في هذه الحالة، يمكنك وضع غالق معتم أعلى السطح، والذي سيختفي عندما يصبح السطح جاهزًا. تتيح لك دالة ContentFrame تخصيص الغالق كدالة lambda لاحقة، ولكنها ستكون تلقائيًا باللون الأسود @Composable Box تملأ حجم الحاوية الرئيسية.

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

يعرف العديد من مطوّري برامج 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(COMMAND_ACTION_A))
    private set

  var someFieldValue by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

  suspend fun observe(): Nothing = player.listen { events ->
    if (events.containsAny(EVENT_B_CHANGED, EVENT_C_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
      someFieldValue = this.someField
      isEnabled = this.isCommandAvailable(COMMAND_ACTION_A)
    }
  }
}

للتفاعل مع Player.Events، يمكنك التقاطها باستخدام Player.listen وهي suspend fun تتيح لك الدخول إلى عالم الكوروتين والاستماع إلى Player.Events إلى أجل غير مسمى. يساعد تنفيذ Media3 لحالات واجهة المستخدم المختلفة المطوّر النهائي على عدم الاهتمام بالتعرّف على Player.Events.