סמנטיקה

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

רשימה מלאה של מאפייני הסמנטיקה זמינה באובייקט 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
    }
)

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

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

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

@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.

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

‎=Visualization of Switch Access navigation on screen
איור 5. הדמיה חזותית של הניווט בגישה באמצעות מתג במסך.

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

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. תצוגה חזותית של תפריט הפעולות של 'גישה באמצעות מתג'.

עץ סמנטיקה

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

התאמת העץ

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