מיזוג וניקוי

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

סמנטיקה של מיזוג

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

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

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

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

כדי לאפשר ל-Compose למזג את הרכיבים האלה, משתמשים בפרמטר mergeDescendants במודификатор הסמנטיקה. כך, שירותי הנגישות מתייחסים לרכיב כישות אחת, וכל מאפייני הסמנטיקה של הצאצאים מתמזגים:

@Composable
private fun PostMetadata(metadata: Metadata) {
    // Merge elements below for accessibility purposes
    Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
        Image(
            imageVector = Icons.Filled.AccountCircle,
            contentDescription = null // decorative
        )
        Column {
            Text(metadata.author.name)
            Text("${metadata.date}${metadata.readTimeMinutes} min read")
        }
    }
}

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

קבוצה של רכיבי ממשק משתמש, כולל שם המשתמש. כל הרכיבים נבחרים יחד.
איור 2. קבוצה של רכיבי ממשק משתמש, כולל שם המשתמש. כל הרכיבים נבחרים יחד.

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

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

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

@Composable
private fun ArticleListItem(
    openArticle: () -> Unit,
    addToBookmarks: () -> Unit,
) {

    Row(modifier = Modifier.clickable { openArticle() }) {
        // Merges with parent clickable:
        Icon(
            painter = painterResource(R.drawable.ic_logo),
            contentDescription = "Article thumbnail"
        )
        ArticleDetails()

        // Defies the merge due to its own clickable:
        BookmarkButton(onClick = addToBookmarks)
    }
}

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

העץ הממוזג מכיל כמה טקסטים ברשימה בתוך צומת השורה. העץ שלא עבר מיזוג מכיל צמתים נפרדים לכל רכיב טקסט.
איור 4. העץ הממוזג מכיל כמה טקסטים ברשימה בתוך הצומת Row. העץ שלא עבר מיזוג מכיל צמתים נפרדים לכל Text composable
.

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

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

ניקוי והגדרה של סמנטיקה

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

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

חשוב לזכור: כשמנקים באמצעות פונקציית lambda ריקה, הסמנטיקה שנמחקת לא נשלחת לצרכנים שמשתמשים במידע הזה, כמו נגישות, מילוי אוטומטי או בדיקה. כשמחליפים תוכן באמצעות clearAndSetSemantics{/*semantic information*/}, הסמנטיקה החדשה מחליפה את כל הסמנטיקה הקודמת של הרכיב ושל הצאצאים שלו.

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

// Developer might intend this to be a toggleable.
// Using `clearAndSetSemantics`, on the Row, a clickable modifier is applied,
// a custom description is set, and a Role is applied.

@Composable
fun FavoriteToggle() {
    val checked = remember { mutableStateOf(true) }
    Row(
        modifier = Modifier
            .toggleable(
                value = checked.value,
                onValueChange = { checked.value = it }
            )
            .clearAndSetSemantics {
                stateDescription = if (checked.value) "Favorited" else "Not favorited"
                toggleableState = ToggleableState(checked.value)
                role = Role.Switch
            },
    ) {
        Icon(
            imageVector = Icons.Default.Favorite,
            contentDescription = null // not needed here

        )
        Text("Favorite?")
    }
}

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

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

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

כשמשתמשים ב-clearAndSetSemantics, חשוב לשים לב לדברים הבאים:

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

הסתרת סמנטיקה

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

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

@Composable
fun WatermarkExample(
    watermarkText: String,
    content: @Composable () -> Unit,
) {
    Box {
        WatermarkedContent()
        // Mark the watermark as hidden to accessibility services.
        WatermarkText(
            text = watermarkText,
            color = Color.Gray.copy(alpha = 0.5f),
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .semantics { hideFromAccessibility() }
        )
    }
}

@Composable
fun DecorativeExample() {
    Text(
        modifier =
        Modifier.semantics {
            hideFromAccessibility()
        },
        text = "A dot character that is used to decoratively separate information, like •"
    )
}

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

פירוט של תרחישים לדוגמה

כדי להבין את ההבדלים בין ממשקי ה-API הקודמים, ריכזנו כאן סיכום של תרחישים לדוגמה:

  • כשהתוכן לא מיועד לשימוש בשירותי נגישות:
    • משתמשים ב-hideFromAccessibility כשהתוכן עשוי להיות דקורטיבי או יתיר, אבל עדיין צריך לבדוק אותו.
    • משתמשים ב-clearAndSetSemantics{} עם פונקציית lambda ריקה כשצריך לנקות את הסמנטיקה של ההורים והצאצאים בכל השירותים.
    • משתמשים ב-clearAndSetSemantics{/*content*/} עם תוכן בתוך הלוגריתם המפנה (lambda) כשצריך להגדיר ידנית את הסמנטיקה של רכיב.
  • כשצריך להתייחס לתוכן כישות אחת, וכל פרטי הצאצאים שלו צריכים להיות מלאים:
    • שימוש בצאצאים סמנטיים של מיזוג.
טבלה עם תרחישי שימוש שונים ב-API.
איור 5. טבלה עם תרחישי שימוש שונים ב-API.