توفّر مكتبة 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 الترتيب الذي يرسم به طبقاته.
Player
تعرض عناصر التحكّم في الوسط والأسفل خانات
PlayerDefaults.CenterControls و
PlayerDefaults.BottomControls. تكون خانات topControls وabove وbelow فارغة تلقائيًا.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إلى حالات غير قانونية محتملة في واجهة المستخدم.
إنشاء حالات مخصّصة لواجهة المستخدم
يمكنك إضافة حالات مخصّصة لواجهة المستخدم إذا لم تكن الحالات الحالية تلبّي احتياجاتك. اطّلِع على الرمز المصدر للحالة الحالية لنسخ النمط. يؤدي عنصر الاحتفاظ بحالة واجهة المستخدم النموذجي ما يلي:
- يتم إدخال عنصر
Player. - يؤدي هذا الإجراء إلى الاشتراك في
Playerباستخدام الكوروتين. يمكنك الاطّلاع علىPlayer.listenلمزيد من التفاصيل. - يستجيب لـ
Player.Eventsمعيّن من خلال تعديل حالته الداخلية. - تقبل هذه الواجهة أوامر منطق النشاط التجاري التي سيتم تحويلها إلى تعديل مناسب على
Player. - يمكن إنشاؤها في أماكن متعددة في شجرة واجهة المستخدم وستحافظ دائمًا على عرض متسق لحالة المشغّل.
- تتيح هذه الميزة عرض حقول Compose
Stateالتي يمكن استخدامها من خلال دالة مركّبة للاستجابة للتغييرات بشكل ديناميكي. - وتتضمّن دالة
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.