הוספת יחסי התלות
ספריית Media3 כוללת מודול ממשק משתמש שמבוסס על Jetpack Compose. כדי להשתמש בו, מוסיפים את יחסי התלות הבאים:
Kotlin
implementation("androidx.media3:media3-ui-compose:1.7.1")
מגניב
implementation "androidx.media3:media3-ui-compose:1.7.1"
מומלץ מאוד לפתח את האפליקציה בגישה של Compose-first או לעבור משימוש ב-Views.
אפליקציית הדגמה מלאה של Compose
ספריית media3-ui-compose
לא כוללת רכיבים שאפשר להשתמש בהם ישר (Composables) כמו לחצנים, אינדיקטורים, תמונות או תיבות דו-שיח, אבל אפשר למצוא אפליקציית הדגמה שנכתבה במלואה ב-Compose בלי להשתמש בפתרונות של יכולת פעולה הדדית כמו עטיפת PlayerView
ב-AndroidView
. אפליקציית ההדגמה משתמשת במחזיקי מצב של ממשק המשתמש ממודול media3-ui-compose
, ומתבססת על ספריית Compose Material3.
מאחסני מצבים לממשקי משתמש
כדי להבין טוב יותר איך אפשר להשתמש בגמישות של מחזיקי מצב בממשק המשתמש לעומת פונקציות Composable, כדאי לקרוא על האופן שבו 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
או שצריך להסתיר אותו באמצעות רכיב placeholder בממשק המשתמש.
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
כדי לשנות את גודל הרכיב בהתאם ליחס הגובה-רוחב הרצוי (מידע נוסף על סוגים נוספים זמין במסמכי ContentScale) וגם ב-presentationState.coverSurface
כדי לדעת מתי התזמון לא מתאים להצגת הרכיב. במקרה כזה, אפשר למקם תריס אטום מעל המשטח, והוא ייעלם כשהמשטח יהיה מוכן.
איפה נמצאים ה-Flows?
מפתחי Android רבים מכירים את השימוש באובייקטים של Kotlin Flow
כדי לאסוף נתונים משתנים של ממשק המשתמש. לדוגמה, יכול להיות שאתם מחפשים Player.isPlaying
flow שאפשר 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
שאפשר להשתמש בהם ב-Composable כדי להגיב באופן דינמי לשינויים. - כולל פונקציה
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
.