הספרייה 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 מספקת קבוצה של רכיבים הניתנים להרכבה מראש עבור אמצעי בקרה נפוצים של נגנים. אלה כמה מהרכיבים שבהם אפשר להשתמש ישירות באפליקציה:
| רכיב | תיאור |
|---|---|
PlayPauseButton |
מאגר מצבים ללחצן שמשנה את המצב שלו בין הפעלה להשהיה. |
SeekBackButton |
מאגר מצב של כפתור להרצה אחורה במרווח מוגדר. |
SeekForwardButton |
קונטיינר של מצב לכפתור שמעביר קדימה במרווח מוגדר. |
NextButton |
מאגר מצב של לחצן שמעביר לפריט המדיה הבא. |
PreviousButton |
מאגר מצבים של כפתור שמעביר לפריט המדיה הקודם. |
RepeatButton |
מאגר מצבים של לחצן שמעביר אתכם בין מצבי חזרה. |
ShuffleButton |
מאגר מצבים ללחצן שמשמש להפעלה או להשבתה של מצב ערבוב. |
MuteButton |
מאגר מצבים של כפתור להשתקה ולביטול ההשתקה של הנגן. |
TimeText |
מאגר מצבים לקומפוזבילי שמציג את התקדמות השחקן. |
ContentFrame |
משטח להצגת תוכן מדיה שמטפל בניהול יחס הגובה-רוחב, בשינוי גודל ובתריס |
PlayerSurface |
משטח גולמי שעוטף את SurfaceView ואת TextureView ב-AndroidView. |
מאחסני מצבים לממשקי משתמש
אם אף אחד מרכיבי ה-scaffolding לא מתאים לצרכים שלכם, אתם יכולים גם להשתמש ישירות באובייקטים של מצב. בדרך כלל מומלץ להשתמש בשיטות remember המתאימות כדי לשמור על המראה של ממשק המשתמש בין קומפוזיציות מחדש.
כדי להבין טוב יותר איך אפשר להשתמש בגמישות של מחזיקי מצב ממשק המשתמש לעומת פונקציות Composable, כדאי לקרוא על האופן שבו Compose מנהל את המצב.
מאחסני מצבים של לחצנים
במצבי ממשק משתמש מסוימים, הספרייה מניחה שהם ישמשו ככל הנראה רכיבים קומפוזביליים דמויי לחצנים.
| מדינה | remember*State | סוג |
|---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
קבוע |
NextButtonState |
rememberNextButtonState |
קבוע |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
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 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, 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 איך שתרצה.
עם זאת, יש כמה חסרונות לשימוש ב-Flows למצב ממשק המשתמש Player. אחת הבעיות העיקריות היא האופי האסינכרוני של העברת הנתונים. אנחנו רוצים להשיג כמה שפחות זמן אחזור בין Player.Event לבין הצריכה שלו בצד ממשק המשתמש, כדי להימנע מהצגת רכיבי ממשק משתמש שלא מסונכרנים עם Player.Event.Player
נקודות נוספות:
- זרימה עם כל
Player.Eventsלא תעמוד בדרישות של עיקרון אחריות יחידה, וכל צרכן יצטרך לסנן את האירועים הרלוונטיים. - כדי ליצור זרימה לכל
Player.Event, צריך לשלב אותם (עםcombine) לכל רכיב בממשק המשתמש. יש מיפוי של הרבה להרבה בין Player.Event לבין שינוי ברכיב UI. השימוש ב-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.