כתיבה מהירה

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

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

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

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