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
Artwork دالة مركّبة تعرض صورة ملف الوسائط الحالي، مثل غلاف الألبوم

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

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

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

مشغّل فيديو يتضمّن خطوطًا خارجية متقطّعة تحدّد خاناته من الأعلى إلى الأسفل: topControls على طول الحافة العلوية، وerrorOverlay فوق الوسط، وcenterControls في الوسط مع أزرار backSecondary وback وcentral وforward وforwardSecondary، والترجمة بالقرب من الأسفل، وbottomControls على طول الحافة السفلية مع الخانات above وleft وprogressSlider وright وbelow. ويملأ 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
  )
}

تعرض الدالة المركّبة Player أيضًا الترجمة، وعندما لا يتوفّر فيديو لعرضه، تعرض العمل الفني لملف الوسائط الحالي. لتخصيص مظهرها، يمكنك تمرير الدوال المركّبة الخاصة بك إلى خانتَي subtitles وartwork. لإخفائها، مرِّر دالة مركّبة فارغة إلى subtitles وnull إلى artwork.

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

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

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

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

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

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

بالإضافة إلى حالتَي التشغيل والإيقاف المؤقت، يعرض PlayPauseButtonState showBuffering وshowReplay وshowRetry. يمكنك استخدامها لعرض مؤشر التخزين المؤقت أثناء تخزين المشغّل مؤقتًا، ورمز إعادة التشغيل بعد انتهاء التشغيل، ورمز إعادة المحاولة بعد حدوث خطأ في التشغيل. ‫showReplay وshowRetry هما تحسينان لـ showPlay: عندما يكون أحدهما true، يكون showPlay أيضًا true، لذا يجب التحقّق منهما قبل showPlay. تعالج طريقة onClick هذه الحالات أيضًا، على سبيل المثال من خلال إعداد المشغّل مرة أخرى بعد حدوث خطأ.

لاختيار الحالات التي تريد عرضها، يمكنك تمرير مجموعة من SHOW_BUFFERING_WHEN_PLAYING وSHOW_BUFFERING_ALWAYS وSHOW_REPLAY_ON_ENDED وSHOW_RETRY_ON_ERROR إلى المَعلمة displayMode في rememberPlayPauseButtonState. يجمع displayMode تلقائيًا بين SHOW_BUFFERING_ALWAYS وSHOW_REPLAY_ON_ENDED وSHOW_RETRY_ON_ERROR، ما يؤدي إلى عرض الحالات الثلاث، وتظهر حالة التخزين المؤقت حتى أثناء إيقاف التشغيل مؤقتًا.

مثال على استخدام 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 معلومات حول حالة الخطأ الحالية للمشغّل، ويمكن استخدامها لعرض تراكب خطأ.

تعرض واجهة PlaybackState حالة التشغيل الأساسية للمشغّل، مثل playbackState وisPlaying وplayerError، بشكل مستقل عن أي مكوّن محدّد لواجهة المستخدم. يمكنك استخدامها لتشغيل تراكبات أو تنسيقات مخصّصة، على سبيل المثال لعرض مؤشر تحميل أثناء تخزين المشغّل مؤقتًا. للحصول على مؤشر Material3 مُعدّ مسبقًا، استخدِم BufferingIndicator.

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 تملأ حجم الحاوية الرئيسية.

يعرض PresentationState أيضًا showArtwork، وهو true عندما يتم تشغيل ملف الوسائط الحالي بدون مقطع فيديو، مثل المحتوى الصوتي فقط. في هذه الحالة، يعرض ContentFrame الدالة المركّبة Artwork، والتي تعرض العمل الفني من MediaMetadata للعنصر. لتخصيص العمل الفني أو إخفائه، يمكنك تمرير الدالة المركّبة الخاصة بك أو null إلى خانة artwork في ContentFrame.

بشكل تلقائي، يحمّل Artwork بيانات العمل الفني المضمّنة في البيانات الوصفية فقط، أو من معرّف موارد منتظم (URI) من النوع content أو file أو android.resource. لتحميل العمل الفني من معرّفات الموارد المنتظمة (URI) البعيدة أو من مصادر أخرى، استخدِم أحد الخيارات التالية:

  • مرِّر BitmapLoader، مثل DataSourceBitmapLoader، إلى Artwork.
  • استخدِم دالة Artwork التي تأخذ painter لتحميل الصور باستخدام المكتبة التي تختارها.

على عكس الدالة المركّبة Player التي تعرض العناوين الفرعية تلقائيًا، تترك الدالة ContentFrame خانة subtitles فارغة. لعرض الترجمة في ContentFrame أو لتخصيص عرض الترجمة في Player، يمكنك الاطّلاع على إشارات المشغّل الحالية باستخدام CuesState وعرضها في خانة subtitles. يضع ContentFrame هذه الخانة ضمن حدود الفيديو الذي تم تغيير حجمه ويرسمها فوق الغالق والعمل الفني، بحيث تظل الترجمة مرئية عندما لا يكون هناك فيديو.

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

TrackSelectionState تساعدك في إنشاء قائمة لنوع واحد من المقاطع، مثل قائمة لغة الصوت أو الترجمة. يمكنك إنشاؤه باستخدام rememberTrackSelectionState، مع تمرير نوع المقطع الصوتي ومجموعة من الفلاتر، مثل TRACK_FILTER_LANGUAGE أو TRACK_FILTER_RESOLUTION، التي تحدّد خصائص المقطع الصوتي التي تميّز الخيارات. اضبط hasOffOption أو hasAutoOption على true لإضافة خيار يوقف نوع المقطع الصوتي أو خيار يتيح للمشغّل اختيار المقاطع الصوتية تلقائيًا.

تعرض الولاية selectionOptions وselectedOption المتاحَين. يشير كل TrackSelectionOption إلى ما إذا كان الجهاز متوافقًا معه وما إذا تم اختياره. يؤدي تمرير خيار إلى selectOption إلى تعديل معلَمات اختيار المقطع الصوتي للمشغّل وفقًا لذلك. قبل عرض القائمة، تحقَّق من canSetTrackSelectionParameters وما إذا كان هناك ما يكفي من الخيارات للاختيار من بينها. على سبيل المثال، توفر قائمة الترجمة التالية خيارًا واحدًا لكل لغة ترجمة وخيارًا لإيقاف الترجمة:

@Composable
fun SubtitleMenu(player: Player, modifier: Modifier = Modifier) {
  val state =
    rememberTrackSelectionState(
      player,
      trackType = C.TRACK_TYPE_TEXT,
      trackFilters = TRACK_FILTER_LANGUAGE,
      hasOffOption = true,
    )
  // Hide the menu if the player can't change tracks or has no subtitles to choose from
  if (!state.canSetTrackSelectionParameters || state.selectionOptions.size < 2) return

  Column(modifier.selectableGroup()) {
    for (option in state.selectionOptions) {
      Row(
        Modifier.fillMaxWidth()
          .heightIn(min = 48.dp)
          .selectable(
            selected = option.isSelected,
            enabled = option.isSupported,
            role = Role.RadioButton,
            onClick = { state.selectOption(option) },
          )
          .padding(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalAlignment = Alignment.CenterVertically,
      ) {
        RadioButton(selected = option.isSelected, onClick = null, enabled = option.isSupported)
        Text(
          text =
            if (option.onOffState == TRACK_SELECTION_OFF) "Off"
            else option.language?.let { Locale.forLanguageTag(it).displayName } ?: "Unknown",
          color =
            if (option.isSupported) Color.Unspecified
            else LocalContentColor.current.copy(alpha = 0.38f),
        )
      }
    }
  }
}

إذا كنت بحاجة إلى الوصول المباشر إلى Tracks وTrackSelectionParameters الخاصَّين باللاعب، استخدِم TrackSelectionParametersState بدلاً من ذلك. لمزيد من المعلومات حول كيفية اختيار المشغّل للمقاطع الصوتية، يُرجى الاطّلاع على اختيار المقاطع الصوتية.

أين يمكن العثور على 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.