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