כתיבה מהירה

ספריית 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 עלול להוביל למצבים לא חוקיים בממשק המשתמש.

יצירת מצבי ממשק משתמש בהתאמה אישית

אם המצבים הקיימים של ממשק המשתמש לא מתאימים לצרכים שלכם, אתם יכולים להוסיף מצבים מותאמים אישית. כדאי לבדוק את קוד המקור של המצב הקיים כדי להעתיק את התבנית. בדרך כלל, מחזיק מצב של ממשק משתמש מבצע את הפעולות הבאות:

  1. מקבלת אובייקט Player.
  2. הרשמה למינוי אל Player באמצעות קורוטינות. פרטים נוספים מופיעים במאמר Player.listen.
  3. מגיב לPlayer.Events מסוים על ידי עדכון המצב הפנימי שלו.
  4. מקבל פקודות של לוגיקה עסקית שיהפכו לעדכון מתאים של Player.
  5. אפשר ליצור אותו בכמה מקומות בעץ ממשק המשתמש, והוא תמיד ישמור על תצוגה עקבית של מצב הנגן.
  6. חשיפת שדות של Compose State שאפשר להשתמש בהם ברכיב קומפוזבילי כדי להגיב באופן דינמי לשינויים.
  7. כולל פונקציה 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.