עקרונות לשיפור הנגישות של אפליקציות (תצוגות)

מושגים ויישום ב-Jetpack פיתוח נייטיב

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

‫Android מספקת כמה שירותי נגישות של המערכת, כולל:

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

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

רכיבי התווית

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

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

רכיבים שאפשר לערוך

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

<!-- The hint text for en-US locale would be
     "Apartment, suite, or building". -->
<EditText
   android:id="@+id/addressLine2"
   android:hint="@string/aptSuiteBuilding" ... />

במקרה כזה, מאפיין android:labelFor של אובייקט View צריך להיות מוגדר למזהה של אלמנט EditText. פרטים נוספים מופיעים בסעיף הבא.

זוגות של רכיבים שבהם רכיב אחד מתאר את השני

בדרך כלל לרכיב EditText יש אובייקט View תואם שמתאר מה המשתמשים צריכים להזין ברכיב EditText. כדי לציין את הקשר הזה, מגדירים את מאפיין android:labelFor של אובייקט View.

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

<!-- Label text for en-US locale would be "Username:" -->
<TextView
   android:id="@+id/usernameLabel" ...
   android:text="@string/username"
   android:labelFor="@+id/usernameEntry" />

<EditText
   android:id="@+id/usernameEntry" ... />

<!-- Label text for en-US locale would be "Password:" -->
<TextView
   android:id="@+id/passwordLabel" ...
   android:text="@string/password
   android:labelFor="@+id/passwordEntry" />

<EditText
   android:id="@+id/passwordEntry"
   android:inputType="textPassword" ... />

רכיבים באוסף

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

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

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

Kotlin

data class MovieRating(val title: String, val starRating: Integer)

class MyMovieRatingsAdapter(private val myData: Array<MovieRating>):
        RecyclerView.Adapter<MyMovieRatingsAdapter.MyRatingViewHolder>() {

    class MyRatingViewHolder(val ratingView: ImageView) :
            RecyclerView.ViewHolder(ratingView)

    override fun onBindViewHolder(holder: MyRatingViewHolder, position: Int) {
        val ratingData = myData[position]
        holder.ratingView.contentDescription = "Movie ${position}: " +
                "${ratingData.title}, ${ratingData.starRating} stars"
    }
}

Java

public class MovieRating {
    private String title;
    private int starRating;
    // ...
    public String getTitle() { return title; }
    public int getStarRating() { return starRating; }
}

public class MyMovieRatingsAdapter
        extends RecyclerView.Adapter<MyAdapter.MyRatingViewHolder> {
    private MovieRating[] myData;


    public static class MyRatingViewHolder extends RecyclerView.ViewHolder {
        public ImageView ratingView;
        public MyRatingViewHolder(ImageView iv) {
            super(iv);
            ratingView = iv;
        }
    }

    @Override
    public void onBindViewHolder(MyRatingViewHolder holder, int position) {
        MovieRating ratingData = myData[position];
        holder.ratingView.setContentDescription("Movie " + position + ": " +
                ratingData.getTitle() + ", " + ratingData.getStarRating() +
                " stars")
    }
}

קבוצות של תוכן קשור

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

בקטע הקוד הבא יש חלקי תוכן שקשורים זה לזה, ולכן מאפיין android:screenReaderFocusable של רכיב המאגר, מופע של ConstraintLayout, מוגדר כ-true, ומאפיין android:focusable של כל אחד מרכיבי TextView הפנימיים מוגדר כ-false:

<!-- In response to a single user interaction, accessibility services announce
     both the title and the artist of the song. -->
<ConstraintLayout
    android:id="@+id/song_data_container" ...
    android:screenReaderFocusable="true">

    <TextView
        android:id="@+id/song_title" ...
        android:focusable="false"
        android:text="@string/my_song_title" />
    <TextView
        android:id="@+id/song_artist"
        android:focusable="false"
        android:text="@string/my_songwriter" />
</ConstraintLayout>

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

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

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

קבוצות מקוננות

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

בקטע הקוד הבא מוצגת שיטה אחת לסימון קבוצות בתוך קבוצות גדולות יותר:

<!-- In response to a single user interaction, accessibility services
     announce the events for a single stage only. -->
<ConstraintLayout
    android:id="@+id/festival_event_table" ... >
    <ConstraintLayout
        android:id="@+id/stage_a_event_column"
        android:screenReaderFocusable="true">

        <!-- UI elements that describe the events on Stage A. -->

    </ConstraintLayout>
    <ConstraintLayout
        android:id="@+id/stage_b_event_column"
        android:screenReaderFocusable="true">

        <!-- UI elements that describe the events on Stage B. -->

    </ConstraintLayout>
</ConstraintLayout>

כותרות בתוך טקסט

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

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

שמות החלוניות של הנגישות

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

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

<!-- Accessibility services receive announcements about content changes
     that are scoped to either the "shopping cart view" section (top) or
     "browse items" section (bottom) -->
<MyShoppingCartView
     android:id="@+id/shoppingCartContainer"
     android:accessibilityPaneTitle="@string/shoppingCart" ... />

<MyShoppingBrowseView
     android:id="@+id/browseItemsContainer"
     android:accessibilityPaneTitle="@string/browseProducts" ... />

אלמנטים דקורטיביים

אם אלמנט בממשק המשתמש קיים רק למטרות של ריווח חזותי או מראה חזותי, צריך להגדיר את המאפיין android:importantForAccessibility שלו לערך "no".

הוספת פעולות נגישות

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

הנגשת כל הפעולות

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

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

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

Kotlin

ViewCompat.addAccessibilityAction(
    // View to add accessibility action
    itemView,
    // Label surfaced to user by an accessibility service
    getText(R.id.archive)
) { _, _ ->
    // Same method executed when swiping on itemView
    archiveItem()
    true
}

Java

ViewCompat.addAccessibilityAction(
    // View to add accessibility action
    itemView,
    // Label surfaced to user by an accessibility service
    getText(R.id.archive),
    (view, arguments) -> {
        // Same method executed when swiping on itemView
        archiveItem();
        return true;
    }
);

With the custom accessibility action implemented, users can access the action through the actions menu.

Make available actions understandable

When a view supports actions such as touch & hold, an accessibility service such as TalkBack announces it as "Double tap and hold to long press."

This generic announcement doesn't give the user any context about what a touch & hold action does.

To make this announcement more descriptive, you can replace the accessibility actions announcement like so:

Kotlin

ViewCompat.replaceAccessibilityAction(
    // View that contains touch & hold action
    itemView,
    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK,
    // Announcement read by TalkBack to surface this action
    getText(R.string.favorite),
    null
)

Java

ViewCompat.replaceAccessibilityAction(
    // View that contains touch & hold action
    itemView,
    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK,
    // Announcement read by TalkBack to surface this action
    getText(R.string.favorite),
    null
);

This results in TalkBack announcing "Double tap and hold to favorite," helping users understand the purpose of the action.

Extend system widgets

Note: When you design your app's UI, use or extend system-provided widgets that are as far down Android's class hierarchy as possible. System-provided widgets that are far down the hierarchy already have most of the accessibility capabilities your app needs. It's easier to extend these system-provided widgets than to create your own from the more generic View, ViewCompat, Canvas, and CanvasCompat classes.

If you must extend View or Canvas directly, which might be necessary for a highly customized experience or a game level, see Make custom views more accessible.

This section uses the example of implementing a special type of Switch called TriSwitch while following best practices around extending system widgets. A TriSwitch object works similarly to a Switch object, except that each instance of TriSwitch allows the user to toggle among three possible states.

Extend from far down the class hierarchy

The Switch object inherits from several framework UI classes in its hierarchy:

View
 TextView
   Button
     CompoundButton
       Switch

הכי טוב שהכיתה החדשה TriSwitch תהיה הרחבה ישירה של הכיתה Switch. כך, מסגרת הנגישות של Android מספקת את רוב יכולות הנגישות שנדרשות למחלקה TriSwitch:

  • פעולות נגישות: מידע למערכת על האופן שבו שירותי נגישות יכולים לדמות כל קלט של משתמשים אפשרי שמבוצע באובייקט של TriSwitch. (הועברה בירושה מ-View).
  • אירועי נגישות: מידע לשירותי נגישות על כל הדרכים האפשריות שבהן המראה של אובייקט TriSwitch יכול להשתנות כשהמסך מתרענן או מתעדכן. (הועברה בירושה מ-View).
  • מאפיינים: פרטים על כל אובייקט TriSwitch, כמו התוכן של כל טקסט שמוצג בו. (הועברה בירושה מ-TextView).
  • פרטי מצב: תיאור של המצב הנוכחי של אובייקט TriSwitch, כמו 'מסומן' או 'לא מסומן'. (הועברה בירושה מ-CompoundButton).
  • תיאור טקסטואלי של המצב: הסבר מבוסס-טקסט של מה שמיוצג בכל מצב. (הועברה בירושה מ-Switch).

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

הגדרת אירועים מותאמים אישית

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

הנחיה כללית היא שבכל פעם שמבטלים את ההגדרה של קריאה חוזרת (callback) שמבוססת על תצוגה, צריך גם להגדיר מחדש את פעולת הנגישות המתאימה על ידי ביטול ההגדרה של ViewCompat.replaceAccessibilityAction(). בבדיקות של האפליקציה, אפשר לאמת את ההתנהגות של הפעולות המוגדרות מחדש האלה על ידי קריאה ל-ViewCompat.performAccessibilityAction().

איך העיקרון הזה יכול לפעול באובייקטים מסוג TriSwitch

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

Kotlin

class TriSwitch(context: Context) : Switch(context) {
    // 0, 1, or 2
    var currentState: Int = 0
        private set

    init {
        updateAccessibilityActions()
    }

    private fun updateAccessibilityActions() {
        ViewCompat.replaceAccessibilityAction(this, ACTION_CLICK,
            action-label) {
            view, args -> moveToNextState()
        })
    }

    private fun moveToNextState() {
        currentState = (currentState + 1) % 3
    }
}

Java

public class TriSwitch extends Switch {
    // 0, 1, or 2
    private int currentState;

    public int getCurrentState() {
        return currentState;
    }

    public TriSwitch() {
        updateAccessibilityActions();
    }

    private void updateAccessibilityActions() {
        ViewCompat.replaceAccessibilityAction(this, ACTION_CLICK,
            action-label, (view, args) -> moveToNextState());
    }

    private void moveToNextState() {
        currentState = (currentState + 1) % 3;
    }
}

מקורות מידע נוספים

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

Codelabs

פוסטים בבלוגים