כתיבה מהירה

הספרייה media3-ui-compose מספקת את הרכיבים הבסיסיים ליצירת ממשק משתמש של מדיה ב-Jetpack Compose. היא מיועדת למפתחים שצריכים יותר אפשרויות להתאמה אישית מאלה שמוצעות בספרייה media3-ui-compose-material3. בדף הזה מוסבר איך להשתמש ברכיבי הליבה ובמאחסני המצב כדי ליצור ממשק משתמש מותאם אישית של נגן מדיה.

שילוב של רכיבי Material3 ורכיבי פיתוח נייטיב בהתאמה אישית

הספרייה 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 מאגר מצב של רכיב Composable שמציג את התקדמות הנגן.
ContentFrame משטח להצגת תוכן מדיה שמטפל בניהול יחס הגובה-רוחב, בשינוי הגודל ובצמצם
PlayerSurface משטח גולמי שעוטף את SurfaceView ואת TextureView ב-AndroidView.

התאמה אישית של הרכיב הקומפוזבילי Player

כשמשתמשים בספרייה media3-ui-compose-material3, הפונקציה הקומפוזבילית Player מספקת חוויית הפעלה מלאה של מדיה. אפשר להתאים אישית את הפריסה שלו על ידי הוספת רכיבים קומפוזביליים משלכם למשבצות התוכן שלו, או להשתמש באובייקט PlayerDefaults כדי להתאים אישית חלקים ספציפיים בממשק המשתמש, כמו TopControls,‏ CenterControls,‏ BottomControls ו-ErrorOverlay.

באיור 1 מוצג המיקום של המשבצות ברכיב הקומפוזבילי Player. באיור 2 מוצג הסדר שבו הוא מצייר את השכבות שלו.

נגן וידאו עם קווים מקווקווים שמסמנים את המיקומים שלו. מלמעלה למטה: topControls לאורך הקצה העליון, errorOverlay מעל המרכז, centerControls באמצע עם הכפתורים backSecondary,‏ back,‏ central,‏ forward ו-forwardSecondary,‏ subtitles ליד החלק התחתון ו-bottomControls לאורך הקצה התחתון עם המשבצות above,‏ left,‏ progressSlider,‏ right ו-below. ‫ContentFrame ממלא את האזור שמאחוריהם.
איור 1. המשבצות של הרכיב הקומפוזבילי Player. אמצעי הבקרה במרכז ובתחתית מציגים את המשבצות של PlayerDefaults.CenterControls ושל PlayerDefaults.BottomControls. כברירת מחדל, המיקומים topControls, ‏ above ו-below ריקים.
תצוגה מפורטת של שש שכבות מוערמות. מלמטה למעלה: 1, PlayerSurface;‏ 2, shutter;‏ 3, artwork;‏ 4, subtitles;‏ 5, errorOverlay;‏ ו-6, topControls,‏ centerControls ו-bottomControls. שכבות 1 עד 4 מרכיבות את ContentFrame, וכל שש השכבות מרכיבות את Player.
איור 2. השכבות של הרכיב ה-composable‏ Player, ממוספרות לפי הסדר שבו הן משורטטות.

לדוגמה, אתם יכולים להשתמש ב-PlayerDefaults.CenterControls כדי לשנות רק את לחצן ההפעלה/השהיה המרכזי, ולהשאיר את שאר אמצעי הבקרה המרכזיים ללא שינוי. אפשר גם להעביר פונקציה קומפוזבילית בהתאמה אישית מלאה לפרמטר כמו topControls, ולהשמיט לחלוטין פרמטרים אחרים כמו bottomControls כדי להשאיר אותם בברירת המחדל שלהם.

@Composable
fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    showControls = showControls,
    topControls = {
      // Fully custom top controls
      Text("My custom title")
    },
    centerControls = {
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = player,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(player, modifier = Modifier.size(64.dp))
        },
      )
    },
    // bottomControls are left as default
  )
}

מאחסני מצבים לממשקי משתמש

אם אף אחד מרכיבי הפיגומים לא עונה על הצרכים שלכם, אתם יכולים גם להשתמש באובייקטים של המצב ישירות. בדרך כלל מומלץ להשתמש בשיטות remember המתאימות כדי לשמור על מראה ממשק המשתמש בין הרכבות מחדש.

כדי להבין טוב יותר איך אתם יכולים להשתמש בגמישות של מאחסני המצבים של ממשק המשתמש לעומת פונקציות ה-Composable, קראו על האופן שבו Compose מנהל את המצב.

מאחסני מצבים של כפתורים

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

מדינה remember*State סוג
PlayPauseButtonState rememberPlayPauseButtonState ‫2 מתגים
PreviousButtonState rememberPreviousButtonState קבוע
NextButtonState rememberNextButtonState קבוע
RepeatButtonState rememberRepeatButtonState מתג תלת-מצבי
ShuffleButtonState rememberShuffleButtonState ‫2 מתגים
PlaybackSpeedState rememberPlaybackSpeedState תפריט או החלפת מצב N

דוגמה לשימוש ב-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 או שצריך להסתיר אותו באמצעות רכיב פלייסהולדר בממשק המשתמש. הפונקציה הקומפוזבילית ContentFrame משלבת את הטיפול ביחס הגובה-רוחב עם הצגת הצמצם מעל משטח שעדיין לא מוכן.

@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 כדי לשנות את קנה המידה של ה-Surface ליחס הגובה-רוחב שנבחר (בקטע קנה מידה של תוכן יש סוגים נוספים) וגם ב-presentationState.coverSurface כדי לדעת מתי לא מתאים להציג את ה-Surface. במקרה כזה, אפשר להציב תריס אטום מעל המשטח, שייעלם כשהמשטח יהיה מוכן. ContentFrame מאפשר לכם להתאים אישית את התריס כ-trailing lambda, אבל כברירת מחדל הוא יהיה @Composable Box שחור שממלא את הגודל של קונטיינר ההורה.

איפה נמצאים ה-Flows?

הרבה מפתחים של Android מכירים את השימוש באובייקטים של Kotlin Flow כדי לאסוף נתונים של ממשק המשתמש שמשתנים כל הזמן. לדוגמה, יכול להיות שאתם מחפשים Player.isPlaying flow שאתם יכולים collect באופן שמותאם למחזור החיים. או משהו כמו Player.eventsFlow כדי לספק לכם Flow<Player.Events> שתוכלו filter איך שתרצו.

עם זאת, לשימוש בתהליכים למצב ממשק המשתמש של Player יש כמה חסרונות. אחד החששות העיקריים הוא האופי האסינכרוני של העברת הנתונים. אנחנו רוצים להגיע לזמן אחזור קצר ככל האפשר בין Player.Event לבין הצריכה שלו בצד של ממשק המשתמש, ולהימנע מהצגת רכיבים בממשק המשתמש שלא מסונכרנים עם Player.

נקודות נוספות:

  • תהליך עם כל ה-Player.Events לא יתאים לעיקרון של אחריות אחת, וכל צרכן יצטרך לסנן את האירועים הרלוונטיים.
  • אם יוצרים Flow לכל Player.Event, צריך לשלב אותם (באמצעות combine) לכל רכיב בממשק המשתמש. יש מיפוי של רבים לרבים בין Player.Event לבין שינוי ברכיב בממשק המשתמש. השימוש ב-combine עלול להוביל את ממשק המשתמש למצבים לא חוקיים.

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

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

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