סמנטיקה

בנוסף למידע העיקרי שרכיב ה-Composable מכיל, כמו מחרוזת טקסט של רכיב Composable מסוג Text, יכול להיות שימושי לקבל מידע נוסף על רכיבים בממשק המשתמש.

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

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

ממשקי ה-API של Material,‏ Compose UI ו-Foundation כוללים סמנטיקה מובנית שמתאימה לתפקיד ולפונקציה הספציפיים שלהם, אבל אתם יכולים גם לשנות את הסמנטיקה הזו בממשקי API קיימים או להגדיר סמנטיקה חדשה לרכיבים בהתאמה אישית, בהתאם לדרישות הספציפיות שלכם.

מאפיינים סמנטיים

מאפיינים סמנטיים מעבירים את המשמעות של הרכיב הניתן לשילוב המתאים. לדוגמה, הרכיב הקומפוזבילי Text מכיל מאפיין סמנטי text, כי זו המשמעות של הרכיב הקומפוזבילי הזה. Icon מכיל מאפיין contentDescription (אם המפתח הגדיר אותו) שמעביר בטקסט את המשמעות של הסמל.

חשוב להבין איך מאפייני הסמנטיקה מעבירים את המשמעות של רכיב ה-composable. כדאי להשתמש בSwitch. כך זה נראה למשתמש:

איור 1. Switch במצב "מופעל" ובמצב "מושבת".

כדי לתאר את המשמעות של הרכיב הזה, אפשר לומר: "זהו מתג, שהוא רכיב שניתן להחלפה במצב 'מופעל'. אפשר ללחוץ עליו כדי לבצע פעולות."

זה בדיוק מה שמאפייני הסמנטיקה עושים. צומת הסמנטיקה של רכיב ה-Switch הזה מכיל את המאפיינים הבאים, כפי שמוצג ב-Layout Inspector:

‫Layout Inspector שמציג את המאפיינים הסמנטיים של רכיב קומפוזבילי מסוג Switch
איור 2. ‫Layout Inspector שמציג את מאפייני הסמנטיקה של Switch composable.

הסמל Role מציין את סוג הרכיב. המאפיין StateDescription מתאר איך צריך להתייחס למצב On. כברירת מחדל, זהו תרגום מקומי של המילה "מופעל", אבל אפשר להפוך אותו לספציפי יותר (לדוגמה, "מופעל") בהתאם להקשר. הערך ToggleableState הוא המצב הנוכחי של המתג. המאפיין OnClick מפנה ל-method שמשמש לאינטראקציה עם הרכיב הזה.

מעקב אחרי מאפייני הסמנטיקה של כל רכיב Composable באפליקציה פותח הרבה אפשרויות יעילות:

  • שירותי הנגישות משתמשים במאפיינים כדי לייצג את ממשק המשתמש שמוצג במסך ולאפשר למשתמשים לקיים איתו אינטראקציה. ב-Switch composable, יכול להיות ש-Talkback יכריז: "מופעל; מתג; לחיצה כפולה כדי להחליף מצב". המשתמש יכול ללחוץ לחיצה כפולה על המסך כדי להשבית את המתג.
  • מסגרת הבדיקה משתמשת במאפיינים כדי למצוא צמתים, לבצע איתם אינטראקציה וליצור טענות נכונות (assertions):
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

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

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

רשימה מלאה של מאפייני סמנטיקה מופיעה באובייקט SemanticsProperties. רשימה מלאה של פעולות נגישות אפשריות מופיעה באובייקט SemanticsActions.

כותרות

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

פוסט בבלוג עם טקסט של מאמר בתוך קונטיינר שאפשר לגלול.
איור 3. פוסט בבלוג עם טקסט המאמר בתוך קונטיינר שניתן לגלול.

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

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

התראות וחלונות קופצים

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

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

PopupAlert(
    message = "You have a new message",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)

ברוב המקרים שבהם רוצים למשוך את תשומת הלב של המשתמשים לזמן קצר להתראות או לתוכן חשוב שמשתנה במסך, כדאי להשתמש ב-liveRegionMode.Polite.

כדאי להשתמש ב-liveRegion.Assertive רק כשצריך, כדי לא להפריע למשתמשים. כדאי להשתמש בו במצבים שבהם חשוב שהמשתמשים יקבלו מידע על תוכן שרלוונטי לזמן מוגבל:

PopupAlert(
    message = "Emergency alert incoming",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Assertive
    }
)

כדי לא להציף את המשתמשים במשוב מתמיד, אל תשתמשו באזורים פעילים (live region) בתוכן שמתעדכן לעיתים קרובות, כמו טיימרים לספירה לאחור.

רכיבים דמויי חלון

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

ShareSheet(
    message = "Choose how to share this photo",
    modifier = Modifier
        .fillMaxWidth()
        .align(Alignment.TopCenter)
        .semantics { paneTitle = "New bottom sheet" }
)

לעיון, אפשר לבדוק איך משתמשים ב-paneTitle ב-Material 3 לרכיבים שלו.

רכיבי השגיאה

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

בדוגמה הזו, TalkBack מקריא את טקסט השגיאה הראשי, ואחריו את ההודעה המורחבת הנוספת:

Error(
    errorText = "Fields cannot be empty",
    modifier = Modifier
        .semantics {
            error("Please add both email and password")
        }
)

רכיבים למעקב אחרי ההתקדמות

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

ProgressInfoBar(
    modifier = Modifier
        .semantics {
            progressBarRangeInfo =
                ProgressBarRangeInfo(
                    current = progress,
                    range = 0F..1F
                )
        }
)

מידע על הרשימה ועל הפריטים

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

אם משתמשים בסמנטיקה של collectionInfo ו-collectionItemInfo ברשימה ובפריטים בהתאמה, ברשימה הארוכה הזו, שירותי הנגישות יכולים להודיע למשתמשים על אינדקס הפריטים שבו הם נמצאים מתוך האוסף הכולל, בנוסף למידע סמנטי טקסטואלי:

MilkyWayList(
    modifier = Modifier
        .semantics {
            collectionInfo = CollectionInfo(
                rowCount = milkyWay.count(),
                columnCount = 1
            )
        }
) {
    milkyWay.forEachIndexed { index, text ->
        Text(
            text = text,
            modifier = Modifier.semantics {
                collectionItemInfo =
                    CollectionItemInfo(index, 0, 0, 0)
            }
        )
    }
}

תיאור המצב

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

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
    val stateSubscribed = stringResource(R.string.subscribed)
    val stateNotSubscribed = stringResource(R.string.not_subscribed)
    Row(
        modifier = Modifier
            .semantics {
                // Set any explicit semantic properties
                stateDescription = if (selected) stateSubscribed else stateNotSubscribed
            }
            .toggleable(
                value = selected,
                onValueChange = { onToggle() }
            )
    ) {
        /* ... */
    }
}

פעולות בהתאמה אישית

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

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

SwipeToDismissBox(
    modifier = Modifier.semantics {
        // Represents the swipe to dismiss for accessibility
        customActions = listOf(
            CustomAccessibilityAction(
                label = "Remove article from list",
                action = {
                    removeArticle()
                    true
                }
            )
        )
    },
    state = rememberSwipeToDismissBoxState(),
    backgroundContent = {}
) {
    ArticleListItem()
}

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

המחשה של תפריט הפעולות של TalkBack
איור 4. המחשה של תפריט הפעולות של TalkBack.

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

=המחשה של הניווט בגישה באמצעות מתג במסך
איור 5. המחשה של הניווט באמצעות גישה באמצעות מתג במסך.

כדי לשפר את חוויית הניווט, שמועילה במיוחד לטכנולוגיות מסייעות שמבוססות על אינטראקציה כמו "גישה באמצעות מתג" או Voice Access, אתם יכולים להשתמש בפעולות בהתאמה אישית בקונטיינר כדי להעביר פעולות ממעבר נפרד לתפריט פעולות נפרד:

ArticleListItemRow(
    modifier = Modifier
        .semantics {
            customActions = listOf(
                CustomAccessibilityAction(
                    label = "Open article",
                    action = {
                        openArticle()
                        true
                    }
                ),
                CustomAccessibilityAction(
                    label = "Add to bookmarks",
                    action = {
                        addToBookmarks()
                        true
                    }
                ),
            )
        }
) {
    Article(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = openArticle,
    )
    BookmarkButton(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = addToBookmarks,
    )
}

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

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

הדגשה של פריט ברשימת המאמרים בגישה באמצעות מתג
איור 6. הדגשה של הפריט ברשימת המאמרים באמצעות "גישה באמצעות מתג".
המחשה של תפריט הפעולות של "גישה באמצעות מתג".
איור 7. המחשה של תפריט הפעולות ב"גישה באמצעות מתג".

עץ סמנטיקה

קומפוזיציה מתארת את ממשק המשתמש של האפליקציה ונוצרת על ידי הפעלת רכיבים קומפוזביליים. הקומפוזיציה היא מבנה דמוי עץ שמורכב מהרכיבים הקומפוזביליים שמתארים את ממשק המשתמש.

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

היררכיה אופיינית של ממשק משתמש ועץ הסמנטיקה שלה
איור 8. היררכיה אופיינית של ממשק משתמש ועץ הסמנטיקה שלה.

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

לדוגמה, נניח שיש לכם את הפונקציה הקומפוזבילית הבאה של יומן בהתאמה אישית:

רכיב Composable של יומן בהתאמה אישית עם רכיבי יום שאפשר לבחור
איור 9. רכיב Composable של לוח שנה בהתאמה אישית עם רכיבים של ימים שאפשר לבחור.

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

עץ ממוזג ולא ממוזג

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

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

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

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

ייצוג סמנטיקה של עלה בודד ממוזג
איור 10. ייצוג סמנטיקה של עלה יחיד ממוזג.

תכנים קומפוזביליים ומשתני שינוי יכולים לציין שהם רוצים למזג את מאפייני הסמנטיקה של הצאצאים שלהם על ידי קריאה ל-Modifier.semantics (mergeDescendants = true) {}. הגדרת המאפיין הזה כ-true מציינת שצריך למזג את מאפייני הסמנטיקה. בדוגמה של Button, הפונקציה הקומפוזבילית של Button משתמשת באופן פנימי ב-modifier של clickable שכולל את ה-modifier הזה של semantics. לכן, הצמתים הצאצאים של הלחצן מוזגו. במסמכי התיעוד בנושא נגישות מוסבר מתי כדאי לשנות את התנהגות המיזוג ברכיב ה-composable.

המאפיין הזה מוגדר לכמה משתני שינוי ורכיבים קומפוזביליים בספריות Foundation ו-Material Compose. לדוגמה, המשנים clickable ו-toggleable ימזגו באופן אוטומטי את הצאצאים שלהם. בנוסף, ה-composable ListItem ימזג את הצאצאים שלו.

בדיקת העץ

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

אפשר לבדוק את שני העצים באמצעות השיטה printToLog(). כברירת מחדל, וכמו בדוגמאות הקודמות, העץ הממוזג נרשם ביומן. כדי להדפיס את העץ בלי המיזוג, מגדירים את הפרמטר useUnmergedTree של ההתאמה onRoot() לערך true:

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

בעזרת Layout Inspector אפשר להציג גם את עץ הסמנטיקה הממוזג וגם את עץ הסמנטיקה הלא ממוזג, על ידי בחירה באפשרות המועדפת במסנן התצוגה:

אפשרויות תצוגה של Layout Inspector, שמאפשרות להציג את עץ הסמנטיקה הממוזג והלא ממוזג
איור 11. אפשרויות תצוגה של Layout Inspector, שמאפשרות להציג את עץ הסמנטיקה הממוזג והלא ממוזג.

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

המאפיינים הסמנטיים מוזגו והוגדרו
איור 12. מאפייני הסמנטיקה מוזגו והוגדרו.

כברירת מחדל, ההתאמות ב-Testing Framework משתמשות בעץ הסמנטיקה הממוזג. לכן, אפשר לבצע פעולות ב-Button על ידי התאמה לטקסט שמוצג בתוכו:

composeTestRule.onNodeWithText("Like").performClick()

כדי לשנות את ההתנהגות הזו, מגדירים את הפרמטר useUnmergedTree של ההתאמות ל-true, כמו בהתאמה onRoot.

התאמת העץ

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