כשכתבתי את המאמר האחרון שלי על Jetpack Compose, אמרתי של Jetpack Compose חסרים כמה רכיבים בסיסיים (לפי דעתי), ואחד מהם הוא התוכנית העזרה.

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

אני שמח לומר שמאז הגירסה 1.1.0 של Compose Material 3, יש לנו תמיכה בתוכנית עזרה בנויה. 👏

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

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

❗️ הגירסה של Material 3 שבה עובדת המאמר הזה היא 1.2.1, ששוחררה ב-6 מרץ 2024

סוגי התוכניות עזרה

עכשיו יש תמיכה בשני סוגים של תוכניות עזרה שונים:

  1. תוכנית עזרה פשוטה</diy1
  2. סימן מדיה עשירה

סימן פשוט

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

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

fun TooltipBox(
    positionProvider: PopupPositionProvider,
    tooltip: @Composable TooltipScope.() -> Unit,
    state: TooltipState,
    modifier: Modifier = Modifier,
    focusable: Boolean = true,
    enableUserInput: Boolean = true,
    content: @Composable () -> Unit,
)

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

  • positionProvider – מסוג PopupPositionProvider, ומשמש כדי לחשב את המיקום של הסימן העזרה.

  • tooltip – כאן ניתן לעצב את הUI של איך ייראה הסימן העזרה.

  • state – הוא מחזיק את המצב הקשור למקרה ספציפי של סימן עזרה. הוא מציג שימושים כמו הצג/הסתר את הסימן העזרה וכשמיוצקים מקרה של אחד מהאינסטנציאציות האלה, ניתן ל声明 אם הסימן העזרה צריך להיות ממצא (אומר שהוא צריך להיות מופיע על המסך עד שמשתמש יבצע פעולה מחוץ לסימן העזרה).

  • תוכן – זהו ה-UI שה-tooltip יציג מעל/מתחת.

הנה דוגמה ליצירת BasicTooltipBox עם כל הפרמטרים הרלוונטיים מלאים:

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun BasicTooltip() {
    val tooltipPosition = TooltipDefaults.rememberPlainTooltipPositionProvider()
    val tooltipState = rememberBasicTooltipState(isPersistent = false)

    BasicTooltipBox(positionProvider = tooltipPosition,
        tooltip =  { Text("Hello World") } ,
        state = tooltipState) {
        IconButton(onClick = { }) {
            Icon(imageVector = Icons.Filled.Favorite, 
                 contentDescription = "Your icon's description")
        }
    }
}

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

Tooltip עשיר

Tooltip מדיה עשיר תופס יותר מקום מ-tooltip פשוט וניתן להשתמש בו כדי לספק יותר הקשר על הפונקציונליות של כפתור אייקון. כאשר ה-tooltip מוצג, אתה יכול להוסיף לו כפתורים וקישורים כדי לספק הסבר נוסף או הגדרות.

הוא נוצר בצורה דומה ל-tooltip פשוט, בתוך TooltipBox, אבל אתה משתמש ב-RichTooltip composable.

TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text("RichTooltip") },
                      caretSize = caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {
                        Text("This is where a description would go.")
                  }
        },
        state = tooltipState) {
        IconButton(onClick = {
            /* אירוע לחיצה של כפתור אייקון */
        }) {
            Icon(imageVector = tooltipIcon,
                contentDescription = "Your icon's description",
                tint = iconColor)
        }
    }

כמה דברים לשים לב על Tooltip עשיר:

  1. Tooltip עשיר תומך בקו קטן (caret).

  2. ניתן להוסיף פעולה (כלומר, כפתור) לכדיון כדי לאפשר למשתמשים לקבל מידע נוסף.

  3. ניתן להוסיף לוגיקה לביטול את הכדיון.

מקרי קצה

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

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

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

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

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RichTooltip() {
    val tooltipPosition = TooltipDefaults.rememberRichTooltipPositionProvider()
    val tooltipState = rememberTooltipState(isPersistent = true)
    val scope = rememberCoroutineScope()

    TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text("RichTooltip") },
                      caretSize = TooltipDefaults.caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()  /// <---- כאן
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {

                  }
        },
        state = tooltipState) {
        IconButton(onClick = {  }) {
            Icon(imageVector = Icons.Filled.Call, contentDescription = "Your icon's description")
        }
    }
}

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

ההגיון שלנו שמערך את פעולת המצב של האיוטיפ לא מוערך, אז איך אנחנו נגדל מחדש את מצב האיוטיפ?

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

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

אם רוצים לראות את האיוטיפים באפליקציה, ניתן לבדוק אותהכאן.

הצגות מקורות