הוספת יחסי התלות
ספריית Media3 כוללת מודול של ממשק משתמש שמבוסס על Jetpack Compose. כדי להשתמש בו, מוסיפים את יחסי התלות הבאים:
Kotlin
implementation("androidx.media3:media3-ui-compose:1.6.0")
Groovy
implementation "androidx.media3:media3-ui-compose:1.6.0"
מומלץ מאוד לפתח את האפליקציה שלכם באמצעות Compose או לעבור משימוש בתצוגות.
אפליקציית הדגמה של Compose במלואה
הספרייה media3-ui-compose
לא כוללת רכיבי Composables מוכנים לשימוש (כמו לחצנים, אינדיקטורים, תמונות או תיבת דו-שיח), אבל יש אפליקציית דמו שנכתבה לגמרי ב-Compose, שבה לא נעשה שימוש בפתרונות של יכולת פעולה הדדית, כמו גלישת PlayerView
ב-AndroidView
. באפליקציית הדגמה נעשה שימוש בספריית Compose Material3 ובכיתות של מחזיק המצב של ממשק המשתמש מהמודול media3-ui-compose
.
מאחסני מצבים בממשק המשתמש
כדי להבין טוב יותר איך אפשר להשתמש בגמישות של מאגרי המצב של ממשק המשתמש לעומת רכיבים שניתנים ליצירה, כדאי לקרוא איך Compose מנהל את המצב.
מאגרי מצבים של לחצנים
לגבי מצבי ממשק משתמש מסוימים, אנחנו מניחים שסביר להניח שהם ישמשו לרכיבי Composables שנראים כמו לחצנים.
מדינה | remember*State | סוג |
---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
קבוע |
NextButtonState |
rememberNextButtonState |
קבוע |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
PlaybackSpeedState |
rememberPlaybackSpeedState |
תפריט או לחצן החלפת מצב N |
דוגמה לשימוש ב-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
או שבהם צריך לכסות אותו באלמנט UI של 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
כדי לשנות את הגודל של 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 שאפשר לצרוך ב-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
שמאפשר לכם להיכנס לעולם של פונקציית ה-coroutine ולהאזין ל-Player.Events
ללא הגבלת זמן. הטמעת Media3 של מצבי ממשק משתמש שונים עוזרת למפתח הקצה לא להטריד את עצמו בלמידה על Player.Events
.