תחילת העבודה עם ממשק משתמש שמבוסס על Compose

הוספת יחסי התלות

ספריית Media3 כוללת מודול של ממשק משתמש שמבוסס על Jetpack Compose. כדי להשתמש בו, מוסיפים את יחסי התלות הבאים:

Kotlin

implementation("androidx.media3:media3-ui-compose:1.6.0")

Groovy

implementation "androidx.media3:media3-ui-compose:1.6.0"

מומלץ מאוד לפתח את האפליקציה שלכם באמצעות Compose או לעבור משימוש בתצוגות.

אפליקציית הדגמה של Compose במלואה

הספרייה media3-ui-compose לא כוללת רכיבי Composables מוכנים לשימוש (כמו לחצנים, אינדיקטורים, תמונות או תיבת דו-שיח), אבל יש אפליקציית דמו שנכתבה לגמרי ב-Compose, שבה לא נעשה שימוש בפתרונות של יכולת פעולה הדדית, כמו גלישת PlayerView ב-AndroidView. באפליקציית הדגמה נעשה שימוש בספריית Compose Material3 ובכיתות של מחזיק המצב של ממשק המשתמש מהמודול media3-ui-compose.

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

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

מאגרי מצבים של לחצנים

לגבי מצבי ממשק משתמש מסוימים, אנחנו מניחים שסביר להניח שהם ישמשו לרכיבי Composables שנראים כמו לחצנים.

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

דוגמה לשימוש ב-PlayPauseButtonState:

@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
  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),
    )
  }
}

שימו לב של-state אין מידע על עיצוב, כמו סמל להפעלה או להשהיה. האחריות היחידה שלו היא להפוך את Player למצב של ממשק המשתמש.

לאחר מכן תוכלו לשלב בין הלחצנים בתצוגה לפי ההעדפה שלכם:

Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}

מאחסני מצבים של פלט חזותי

PresentationState מכיל מידע לגבי המצבים שבהם ניתן להציג את פלט הווידאו ב-PlayerSurface או שבהם צריך לכסות אותו באלמנט UI של placeholder.

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(ContentScale.Fit, presentationState.videoSizeDp)

Box(modifier) {
  // 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 = player,
    surfaceType = SURFACE_TYPE_SURFACE_VIEW,
    modifier = scaledModifier,
  )

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    Box(Modifier.background(Color.Black))
  }

כאן אפשר להשתמש ב-presentationState.videoSizeDp כדי לשנות את הגודל של Surface ליחס גובה-רוחב רצוי (במסמכי העזרה של ContentScale מפורטים סוגים נוספים) וב-presentationState.coverSurface כדי לדעת מתי הזמן לא מתאים להציג את Surface. במקרה כזה, אפשר למקם תריס אטום מעל המשטח, שייעלם כשהמשטח יהיה מוכן.

איפה נמצאים התהליכים?

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

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

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

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

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

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

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