ספריית media3-ui-compose מספקת את רכיבי הבסיס ליצירת ממשק משתמש של מדיה ב-Jetpack פיתוח נייטיב. היא מיועדת למפתחים שזקוקים לאפשרויות התאמה אישית נוספות מעבר לאלה שמוצעות בספרייה 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 מספקת קבוצה של רכיבים מובנים שניתן להרכבה (composables) עבור אמצעי בקרה נפוצים של נגנים. אלה כמה מהרכיבים שבהם אפשר להשתמש ישירות באפליקציה:
| רכיב | תיאור |
|---|---|
PlayPauseButton |
מאגר מצבים ללחצן שמשנה את המצב שלו בין הפעלה להשהיה. |
SeekBackButton |
מאגר מצב של כפתור להרצה אחורה במרווח מוגדר. |
SeekForwardButton |
קונטיינר של מצב לכפתור שמעביר קדימה בתוספת מוגדרת. |
NextButton |
מאגר מצב ללחצן שמעביר לפריט המדיה הבא. |
PreviousButton |
מאגר מצב של כפתור שמעביר לפריט המדיה הקודם. |
RepeatButton |
מאגר מצבים של כפתור שמעביר את המשתמש בין מצבי חזרה. |
ShuffleButton |
מאגר מצבים ללחצן שמעביר למצב ערבוב. |
MuteButton |
מאגר מצבים של כפתור להשתקה ולביטול ההשתקה של הנגן. |
TimeText |
מאגר מצבים לרכיב קומפוזבילי שמציג את התקדמות השחקן. |
ContentFrame |
משטח להצגת תוכן מדיה שמטפל בניהול יחס הגובה-רוחב, בשינוי גודל ובסגר |
PlayerSurface |
משטח גולמי שעוטף את SurfaceView ואת TextureView ב-AndroidView. |
התאמה אישית של הרכיב הקומפוזבילי Player
כשמשתמשים בספרייה media3-ui-compose-material3, הקוד הקומפזבילי Player מספק חוויית הפעלת מדיה מלאה. אתם יכולים להתאים אישית את הפריסה שלו על ידי הוספת רכיבים מורכבים משלכם למקומות שמוגדרים לתוכן, או להשתמש באובייקט PlayerDefaults כדי להתאים אישית חלקים ספציפיים בממשק המשתמש, כמו TopControls, CenterControls, BottomControls ו-ErrorOverlay.
לדוגמה, אפשר להשתמש ב-PlayerDefaults.CenterControls כדי לשנות רק את לחצן ההפעלה/ההשהיה המרכזי, בלי לשנות את שאר אמצעי הבקרה המרכזיים.
אפשר גם להעביר קומפוזיציה מותאמת אישית מלאה לפרמטר כמו topControls,
ולהשמיט לגמרי פרמטרים אחרים כמו bottomControls כדי להשאיר אותם בערך ברירת המחדל שלהם.
@Composable fun CustomPlayerSlots(player: Player, modifier: Modifier = Modifier) { Player( player = player, modifier = modifier, topControls = { p, visible -> // Fully custom top controls AnimatedVisibility(visible) { Text("My custom title") } }, centerControls = { p, visible -> // Use default CenterControls but override the central button PlayerDefaults.CenterControls( player = p, visible = visible, central = { // A custom play/pause button Material3PlayPauseButton(it, modifier = Modifier.size(64.dp)) }, ) }, // bottomControls are left as default ) }
מאחסני מצב של ממשק המשתמש
אם אף אחד מרכיבי ה-scaffolding לא מתאים לצרכים שלכם, אתם יכולים גם להשתמש ישירות באובייקטים של מצב. בדרך כלל מומלץ להשתמש בשיטות remember המתאימות כדי לשמור על מראה ממשק המשתמש בין קומפוזיציות מחדש.
כדי להבין טוב יותר איך אפשר להשתמש בגמישות של רכיבי UI לניהול מצב לעומת פונקציות Composable, כדאי לקרוא על האופן שבו Compose מנהל את המצב.
מאחסני מצב של לחצנים
במצבי ממשק משתמש מסוימים, הספרייה מניחה שהרכיבים הקומפוזביליים ישמשו בעיקר כרכיבים קומפוזביליים דמויי לחצנים.
| מדינה | 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), ) }
מאחסני מצב של פלט חזותי
CurrentMediaItemState מספק מידע על MediaItem שמתנגן כרגע, ואילו PlaylistState חושף מידע על MediaItems שהוגדר בנגן. הם שימושיים להצגת מידע על מטא-נתונים בממשק משתמש מותאם אישית.
ErrorState מספק מידע על מצב השגיאה הנוכחי של נגן, שאפשר להשתמש בו כדי להציג שכבת-על של שגיאה.
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.videoAspectRatio) // 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.videoAspectRatio כדי לשנות את גודל הפלטפורמה בהתאם ליחס הגובה-רוחב שנבחר (ראו שינוי גודל התוכן למידע על סוגים נוספים) וגם ב-presentationState.coverSurface כדי לדעת מתי התזמון לא מתאים להצגת הפלטפורמה. במקרה כזה, אפשר למקם תריס אטום מעל המשטח, והוא ייעלם כשהמשטח יהיה מוכן.
ContentFrame מאפשר להתאים אישית את התריס כביטוי למבדא (trailing lambda), אבל כברירת מחדל הוא יהיה @Composable Box שחור שימלא את הגודל של מאגר האב.
איפה נמצאים ה-Flows?
מפתחי Android רבים מכירים את השימוש באובייקטים של Kotlin Flow כדי לאסוף נתונים של ממשק משתמש שמשתנים כל הזמן. לדוגמה, יכול להיות שאתם מחפשים Player.isPlaying flow שאפשר collect באופן שמודע למחזור החיים. או משהו כמו Player.eventsFlow כדי לספק לכם Flow<Player.Events> שתוכלו filter איך שתרצו.
עם זאת, לשימוש ב-Flows לניהול מצב ממשק המשתמש Player יש כמה חסרונות. אחת הבעיות העיקריות היא האופי האסינכרוני של העברת הנתונים. אנחנו רוצים להשיג כמה שפחות זמן אחזור בין Player.Event לבין הצריכה שלו בצד ממשק המשתמש, כדי להימנע מהצגת רכיבי ממשק משתמש שלא מסונכרנים עם Player.Event.Player
נקודות נוספות:
- זרימה עם כל
Player.Eventsלא תעמוד בעקרון האחריות היחידה, וכל צרכן יצטרך לסנן את האירועים הרלוונטיים. - כדי ליצור רצף לכל
Player.Event, צריך לשלב אותם (עםcombine) לכל רכיב בממשק המשתמש. יש מיפוי של הרבה לא הרבה בין Player.Event לבין שינוי ברכיב ממשק המשתמש. השימוש ב-combineעלול להוביל למצבים לא חוקיים בממשק המשתמש.
יצירת מצבי ממשק משתמש בהתאמה אישית
אם המצבים הקיימים של ממשק המשתמש לא מתאימים לצרכים שלכם, אתם יכולים להוסיף מצבים מותאמים אישית. כדאי לבדוק את קוד המקור של המצב הקיים כדי להעתיק את התבנית. בדרך כלל, מחזיק מצב של ממשק משתמש מבצע את הפעולות הבאות:
- מקבלת אובייקט
Player. - הרשמה למינוי אל
Playerבאמצעות קורוטינות. פרטים נוספים מופיעים במאמרPlayer.listen. - מגיב ל
Player.Eventsמסוים על ידי עדכון המצב הפנימי שלו. - מקבל פקודות של לוגיקה עסקית שיהפכו לעדכון מתאים של
Player. - אפשר ליצור אותו בכמה מקומות בעץ ממשק המשתמש, והוא תמיד ישמור על תצוגה עקבית של מצב הנגן.
- חשיפת שדות של Compose
Stateשאפשר להשתמש בהם ברכיב קומפוזבילי כדי להגיב באופן דינמי לשינויים. - כולל פונקציה
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 שמאפשר לכם להיכנס לעולם של קורוטינות ולהאזין ל-Player.Events ללא הגבלה. ההטמעה של Media3 במצבי ממשק משתמש שונים עוזרת למפתחים לא לדאוג לגבי לימוד של Player.Events.