إضافة التبعية
تتضمّن مكتبة Media3 وحدة واجهة مستخدم مستندة إلى Jetpack Compose. لاستخدامها، أضِف التبعية التالية:
Kotlin
implementation("androidx.media3:media3-ui-compose:1.7.1")
Groovy
implementation "androidx.media3:media3-ui-compose:1.7.1"
ننصحك بشدة بتطوير تطبيقك باستخدام 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.resize(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
إلى حالات غير قانونية محتملة في واجهة المستخدم.
إنشاء حالات واجهة مستخدم مخصّصة
يمكنك إضافة حالات واجهة مستخدم مخصّصة إذا لم تلبِّ الحالات الحالية احتياجاتك. اطّلِع على رمز المصدر للحالة الحالية لنسخ النمط. يؤدّي صف حامل حالة واجهة المستخدم النموذجية ما يلي:
- تستقبل
Player
. - يتم الاشتراك في
Player
باستخدام إجراءات فرعية. يمكنك الاطّلاع على المزيد من التفاصيل فيPlayer.listen
. - يستجيب
Player.Events
معيّن من خلال تعديل حالته الداخلية. - قبول أوامر منطق النشاط التجاري التي سيتم تحويلها إلى
Player
تعديل مناسب - يمكن إنشاؤها في مواضع متعددة ضمن شجرة واجهة المستخدِم، وستحافظ دائمًا على عرض متسق لحالة اللاعب.
- تعرض هذه السمة حقول
State
في Compose التي يمكن أن تستهلكها عناصر قابلة للإنشاء من أجل الاستجابة ديناميكيًا للتغييرات. - تتضمّن هذه السمة الدالة
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
.