הספרייה 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. |
מאחסני מצב של ממשק המשתמש
אם אף אחד מרכיבי ה-scaffolding לא מתאים לצרכים שלכם, אתם יכולים גם להשתמש ישירות באובייקטים של state. בדרך כלל מומלץ להשתמש בשיטות התואמות remember כדי לשמור על מראה ממשק המשתמש בין הרכבות מחדש.
כדי להבין טוב יותר איך אפשר להשתמש בגמישות של מאחסני מצב ממשק המשתמש לעומת פונקציות Composable, כדאי לקרוא על האופן שבו פיתוח נייטיב מנהל את המצב.
מאחסני מצבים של לחצנים
במצבי ממשק משתמש מסוימים, הספרייה מניחה שהרכיבים הקומפוזביליים שיוצגו יהיו דומים ללחצנים.
| מדינה | remember*State | סוג |
|---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-החלפת מצב |
PreviousButtonState |
rememberPreviousButtonState |
קבוע |
NextButtonState |
rememberNextButtonState |
קבוע |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-החלפת מצב |
PlaybackSpeedState |
rememberPlaybackSpeedState |
תפריט או N-Toggle |
דוגמה לשימוש ב-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), ) }
מאחסני מצב של פלט חזותי
PresentationState מכיל מידע לגבי המועד שבו אפשר להציג את פלט הווידאו ב-PlayerSurface או שצריך להסתיר אותו באמצעות רכיב placeholder בממשק המשתמש.
ContentFrame Composable משלב את הטיפול ביחס הגובה-רוחב עם הטיפול בהצגת הצמצם על פלטפורמה שעדיין לא מוכנה.
@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.videoSizeDp) // 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.videoSizeDp כדי לשנות את גודל הפלטפורמה בהתאם ליחס הגובה-רוחב שנבחר (במסמכי ContentScale יש סוגים נוספים), וגם ב-presentationState.coverSurface כדי לדעת מתי התזמון לא מתאים להצגת הפלטפורמה. במקרה כזה, אפשר למקם תריס אטום מעל המשטח, והוא ייעלם כשהמשטח יהיה מוכן. ContentFrame
מאפשר להגדיר את התריס כ-lambda נגרר, אבל כברירת מחדל הוא יהיה @Composable Box שחור בגודל של מאגר האב.
איפה נמצאים ה-Flows?
מפתחי Android רבים מכירים את השימוש באובייקטים של Kotlin Flow כדי לאסוף נתונים של ממשק משתמש שמשתנים כל הזמן. לדוגמה, יכול להיות שאתם מחפשים Player.isPlaying flow שאפשר collect באופן שמודע למחזור החיים. או משהו כמו Player.eventsFlow כדי לספק לכם Flow<Player.Events> שתוכלו filter איך שתרצו.
עם זאת, יש כמה חסרונות לשימוש בתהליכים להגדרת מצב ממשק המשתמש Player. אחת הבעיות העיקריות היא האופי האסינכרוני של העברת הנתונים. אנחנו רוצים להשיג את זמן האחזור הקצר ביותר האפשרי בין Player.Event לבין הצריכה שלו בצד ממשק המשתמש, כדי להימנע מהצגת רכיבי ממשק משתמש שלא מסונכרנים עם 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(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 שמאפשר לכם להיכנס לעולם של שגרות המשך (coroutine) ולהאזין ל-Player.Events ללא הגבלה. ההטמעה של Media3 במצבי ממשק משתמש שונים עוזרת למפתחים לא לדאוג לגבי לימוד של .Player.Events