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.

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

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

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

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

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

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

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

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

مثال على استخدام 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 holds to information for when the video output in a PlayerSurface can be shown or should be covered by a placeholder UI element. 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 لتغيير حجم السطح إلى نسبة العرض إلى الارتفاع المختارة (راجِع تغيير حجم المحتوى لمزيد من الأنواع) و presentationState.coverSurface لمعرفة الوقت غير المناسب لـ عرض السطح. في هذه الحالة، يمكنك وضع ستارة غير شفافة أعلى السطح، والتي ستختفي عندما يصبح السطح جاهزًا. ContentFrame يتيح لك تخصيص الستارة كدالة لامدا لاحقة، ولكنّها ستكون تلقائيًا @Composable Box أسودًا يملأ حجم الحاوية الرئيسية.

أين تتوفّر المهام؟

اعتاد العديد من مطوّري 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(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.