יצירת שירות נגישות משלכם (תצוגות)

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

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

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

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

יצירת שירות הנגישות

בתוך הפרויקט, יוצרים מחלקה שמרחיבה את AccessibilityService:

Kotlin

package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent

class MyAccessibilityService : AccessibilityService() {
...
    override fun onInterrupt() {}

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
...
}

Java

package com.example.android.apis.accessibility;

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class MyAccessibilityService extends AccessibilityService {
...
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    }

    @Override
    public void onInterrupt() {
    }

...
}

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

הצהרות והרשאות במניפסט

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

הצהרה על שירותי נגישות

כדי שהאפליקציה תוגדר כשירות נגישות, צריך לכלול אלמנט service במקום אלמנט activity בתוך אלמנט application במניפסט. בנוסף, בתוך רכיב service, צריך לכלול מסנן intent של שירות נגישות. בנוסף, קובץ המניפסט צריך להגן על השירות על ידי הוספת ההרשאה BIND_ACCESSIBILITY_SERVICE כדי לוודא שרק המערכת יכולה לקשר אותו. הנה דוגמה:

  <application>
    <service android:name=".MyAccessibilityService"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
        android:label="@string/accessibility_service_label">
      <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
    </service>
  </application>

הגדרת שירות נגישות

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

אפשר לכלול רכיב <meta-data> במניפסט עם הפניה לקובץ הגדרה, שמאפשר להגדיר את כל האפשרויות של שירות הנגישות, כמו בדוגמה הבאה:

<service android:name=".MyAccessibilityService">
  ...
  <meta-data
    android:name="android.accessibilityservice"
    android:resource="@xml/accessibility_service_config" />
</service>

הרכיב <meta-data> מפנה לקובץ XML שיוצרים בספריית המשאבים של האפליקציה: <project_dir>/res/xml/accessibility_service_config.xml>. הקוד הבא מציג דוגמה לתוכן של קובץ הגדרות השירות:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_service_description"
    android:packageNames="com.example.android.apis"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"
/>

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

מידע נוסף על הגדרות התצורה שאפשר להגדיר באופן דינמי בזמן הריצה מופיע במאמרי העזרה של AccessibilityServiceInfo.

הגדרת שירות הנגישות

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

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

יש שתי אפשרויות להגדרת המשתנים האלה. האפשרות שתואמת לדור קודם היא להגדיר אותם בקוד באמצעות setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo). כדי לעשות זאת, מחליפים את השיטה onServiceConnected() ומגדירים את השירות שם, כמו בדוגמה הבאה:

Kotlin

override fun onServiceConnected() {
    info.apply {
        // Set the type of events that this service wants to listen to. Others
        // aren't passed to this service.
        eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

        // If you only want this service to work with specific apps, set their
        // package names here. Otherwise, when the service is activated, it
        // listens to events from all apps.
        packageNames = arrayOf("com.example.android.myFirstApp", "com.example.android.mySecondApp")

        // Set the type of feedback your service provides.
        feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN

        // Default services are invoked only if no package-specific services are
        // present for the type of AccessibilityEvent generated. This service is
        // app-specific, so the flag isn't necessary. For a general-purpose
        // service, consider setting the DEFAULT flag.

        // flags = AccessibilityServiceInfo.DEFAULT;

        notificationTimeout = 100
    }

    this.serviceInfo = info

}

Java

@Override
public void onServiceConnected() {
    // Set the type of events that this service wants to listen to. Others
    // aren't passed to this service.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
            AccessibilityEvent.TYPE_VIEW_FOCUSED;

    // If you only want this service to work with specific apps, set their
    // package names here. Otherwise, when the service is activated, it listens
    // to events from all apps.
    info.packageNames = new String[]
            {"com.example.android.myFirstApp", "com.example.android.mySecondApp"};

    // Set the type of feedback your service provides.
    info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;

    // Default services are invoked only if no package-specific services are
    // present for the type of AccessibilityEvent generated. This service is
    // app-specific, so the flag isn't necessary. For a general-purpose service,
    // consider setting the DEFAULT flag.

    // info.flags = AccessibilityServiceInfo.DEFAULT;

    info.notificationTimeout = 100;

    this.setServiceInfo(info);

}

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

<accessibility-service
     android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
     android:packageNames="com.example.android.myFirstApp, com.example.android.mySecondApp"
     android:accessibilityFeedbackType="feedbackSpoken"
     android:notificationTimeout="100"
     android:settingsActivity="com.example.android.apis.accessibility.TestBackActivity"
     android:canRetrieveWindowContent="true"
/>

אם משתמשים ב-XML, צריך להוסיף תג <meta-data> להצהרת השירות במניפסט כדי להפנות לקובץ ה-XML. אם קובץ ה-XML מאוחסן ב-res/xml/serviceconfig.xml, התג החדש ייראה כך:

<service android:name=".MyAccessibilityService">
     <intent-filter>
         <action android:name="android.accessibilityservice.AccessibilityService" />
     </intent-filter>
     <meta-data android:name="android.accessibilityservice"
     android:resource="@xml/serviceconfig" />
</service>

שיטות של שירותי נגישות

שירות נגישות צריך להרחיב את המחלקה AccessibilityService ולשנות את ההגדרות של ה-methods הבאים מהמחלקה הזו. השיטות האלה מוצגות בסדר שבו מערכת Android קוראת להן: מהרגע שהשירות מתחיל (onServiceConnected()), בזמן שהוא פועל (onAccessibilityEvent(), onInterrupt()), ועד שהוא מושבת (onUnbind()).

  • onServiceConnected(): (אופציונלי) המערכת קוראת לשיטה הזו כשהיא מתחברת לשירות הנגישות שלכם. משתמשים בשיטה הזו כדי לבצע שלבי הגדרה חד-פעמיים לשירות, כולל התחברות לשירותים של מערכת משוב משתמשים, כמו מנהל האודיו או מנגנון הרטט של המכשיר. אם רוצים להגדיר את השירות בזמן הריצה או לבצע שינויים חד-פעמיים, אפשר לבצע קריאות ל-setServiceInfo() במיקום הזה.

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

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

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

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

הרשמה לאירועי נגישות

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

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

  • סוגי אירועים: מציינים את סוגי אירועי הנגישות שרוצים שהשירות יטפל בהם. אפשר להגדיר את הפרמטר הזה בקובצי ההגדרות של שירות הנגישות באמצעות המאפיין android:accessibilityEventTypes כרשימה שמופרדת באמצעות התו | – לדוגמה, accessibilityEventTypes="typeViewClicked|typeViewFocused". אפשר גם להגדיר אותו באמצעות חבר AccessibilityServiceInfo.eventTypes.

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

עוצמת הקול לנגישות

במכשירים עם Android 8.0 (רמת API‏ 26) ומעלה, יש קטגוריית עוצמת קול STREAM_ACCESSIBILITY, שמאפשרת לשלוט בעוצמת הקול של פלט האודיו של שירות הנגישות בנפרד מצלילים אחרים במכשיר.

שירותי נגישות יכולים להשתמש בסוג הסטרימינג הזה על ידי הגדרת האפשרות FLAG_ENABLE_ACCESSIBILITY_VOLUME. אחר כך אפשר לשנות את עוצמת הקול של האודיו לנגישות במכשיר על ידי קריאה לשיטה adjustStreamVolume() במופע של AudioManager במכשיר.

בקטע הקוד הבא אפשר לראות איך שירות נגישות יכול להשתמש בקטגוריית עוצמת הקול STREAM_ACCESSIBILITY:

Kotlin

import android.media.AudioManager.*

class MyAccessibilityService : AccessibilityService() {

    private val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager

    override fun onAccessibilityEvent(accessibilityEvent: AccessibilityEvent) {
        if (accessibilityEvent.source.text == "Increase volume") {
            audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY, ADJUST_RAISE, 0)
        }
    }
}

Java

import static android.media.AudioManager.*;

public class MyAccessibilityService extends AccessibilityService {
    private AudioManager audioManager =
            (AudioManager) getSystemService(AUDIO_SERVICE);

    @Override
    public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
        AccessibilityNodeInfo interactedNodeInfo =
                accessibilityEvent.getSource();
        if (interactedNodeInfo.getText().equals("Increase volume")) {
            audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY,
                ADJUST_RAISE, 0);
        }
    }
}

מידע נוסף זמין בסרטון של סשן Google I/O 2017 בנושא What's new in Android accessibility (מה חדש בנגישות ב-Android), החל מדקה 6:35.

קיצור דרך לנגישות

במכשירים עם Android 8.0 (רמת API 26) ומעלה, המשתמשים יכולים להפעיל ולהשבית את שירות הנגישות המועדף שלהם מכל מסך על ידי לחיצה ממושכת על שני לחצני עוצמת הקול בו-זמנית. למרות שקיצור הדרך הזה מפעיל ומשבית את TalkBack כברירת מחדל, המשתמשים יכולים להגדיר את הלחצן להפעלה ולהשבתה של כל שירות שמותקן במכשיר שלהם.

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

למידע נוסף, אפשר לצפות בסרטון של הסשן What's new in Android accessibility (מה חדש בנגישות ב-Android) מ-Google I/O 2017, החל מדקה 13:25.

כפתור הנגישות

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

כדי לאפשר למשתמשים להפעיל שירות נגישות מסוים באמצעות לחצן הנגישות, השירות צריך להוסיף את הדגל FLAG_REQUEST_ACCESSIBILITY_BUTTON במאפיין android:accessibilityFlags של אובייקט AccessibilityServiceInfo. לאחר מכן, השירות יכול לרשום קריאות חוזרות באמצעות registerAccessibilityButtonCallback().

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

Kotlin

private var mAccessibilityButtonController: AccessibilityButtonController? = null
private var accessibilityButtonCallback:
        AccessibilityButtonController.AccessibilityButtonCallback? = null
private var mIsAccessibilityButtonAvailable: Boolean = false

override fun onServiceConnected() {
    mAccessibilityButtonController = accessibilityButtonController
    mIsAccessibilityButtonAvailable =
            mAccessibilityButtonController?.isAccessibilityButtonAvailable ?: false

    if (!mIsAccessibilityButtonAvailable) return

    serviceInfo = serviceInfo.apply {
        flags = flags or AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON
    }

    accessibilityButtonCallback =
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!")

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            override fun onAvailabilityChanged(
                    controller: AccessibilityButtonController,
                    available: Boolean
            ) {
                if (controller == mAccessibilityButtonController) {
                    mIsAccessibilityButtonAvailable = available
                }
            }
    }

    accessibilityButtonCallback?.also {
        mAccessibilityButtonController?.registerAccessibilityButtonCallback(it, null)
    }
}

Java

private AccessibilityButtonController accessibilityButtonController;
private AccessibilityButtonController
        .AccessibilityButtonCallback accessibilityButtonCallback;
private boolean mIsAccessibilityButtonAvailable;

@Override
protected void onServiceConnected() {
    accessibilityButtonController = getAccessibilityButtonController();
    mIsAccessibilityButtonAvailable =
            accessibilityButtonController.isAccessibilityButtonAvailable();

    if (!mIsAccessibilityButtonAvailable) {
        return;
    }

    AccessibilityServiceInfo serviceInfo = getServiceInfo();
    serviceInfo.flags
            |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
    setServiceInfo(serviceInfo);

    accessibilityButtonCallback =
        new AccessibilityButtonController.AccessibilityButtonCallback() {
            @Override
            public void onClicked(AccessibilityButtonController controller) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!");

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            @Override
            public void onAvailabilityChanged(
              AccessibilityButtonController controller, boolean available) {
                if (controller.equals(accessibilityButtonController)) {
                    mIsAccessibilityButtonAvailable = available;
                }
            }
        };

    if (accessibilityButtonCallback != null) {
        accessibilityButtonController.registerAccessibilityButtonCallback(
                accessibilityButtonCallback, null);
    }
}

מידע נוסף זמין בסרטון של ההרצאה What's new in Android accessibility מ-Google I/O 2017, החל מדקה 16:28.

תנועות של טביעות אצבעות

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

  1. מצהירים על ההרשאה USE_BIOMETRIC ועל היכולת CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES.
  2. מגדירים את הדגל FLAG_REQUEST_FINGERPRINT_GESTURES במאפיין android:accessibilityFlags.
  3. נרשמים לקבלת שיחות חוזרות באמצעות registerFingerprintGestureCallback() ).

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

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

// AndroidManifest.xml
<manifest ... >
    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
    ...
    <application>
        <service android:name="com.example.MyFingerprintGestureService" ... >
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/myfingerprintgestureservice" />
        </service>
    </application>
</manifest>
// myfingerprintgestureservice.xml
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:accessibilityFlags=" ... |flagRequestFingerprintGestures"
    android:canRequestFingerprintGestures="true"
    ... />

Kotlin

// MyFingerprintGestureService.kt
import android.accessibilityservice.FingerprintGestureController.*

class MyFingerprintGestureService : AccessibilityService() {

    private var gestureController: FingerprintGestureController? = null
    private var fingerprintGestureCallback:
            FingerprintGestureController.FingerprintGestureCallback? = null
    private var mIsGestureDetectionAvailable: Boolean = false

    override fun onCreate() {
        gestureController = fingerprintGestureController
        mIsGestureDetectionAvailable = gestureController?.isGestureDetectionAvailable ?: false
    }

    override fun onServiceConnected() {
        if (mFingerprintGestureCallback != null || !mIsGestureDetectionAvailable) return

        fingerprintGestureCallback =
                object : FingerprintGestureController.FingerprintGestureCallback() {
                    override fun onGestureDetected(gesture: Int) {
                        when (gesture) {
                            FINGERPRINT_GESTURE_SWIPE_DOWN -> moveGameCursorDown()
                            FINGERPRINT_GESTURE_SWIPE_LEFT -> moveGameCursorLeft()
                            FINGERPRINT_GESTURE_SWIPE_RIGHT -> moveGameCursorRight()
                            FINGERPRINT_GESTURE_SWIPE_UP -> moveGameCursorUp()
                            else -> Log.e(MY_APP_TAG, "Error: Unknown gesture type detected!")
                        }
                    }

                    override fun onGestureDetectionAvailabilityChanged(available: Boolean) {
                        mIsGestureDetectionAvailable = available
                    }
                }

        fingerprintGestureCallback?.also {
            gestureController?.registerFingerprintGestureCallback(it, null)
        }
    }
}

Java

// MyFingerprintGestureService.java
import static android.accessibilityservice.FingerprintGestureController.*;

public class MyFingerprintGestureService extends AccessibilityService {
    private FingerprintGestureController gestureController;
    private FingerprintGestureController
            .FingerprintGestureCallback fingerprintGestureCallback;
    private boolean mIsGestureDetectionAvailable;

    @Override
    public void onCreate() {
        gestureController = getFingerprintGestureController();
        mIsGestureDetectionAvailable =
                gestureController.isGestureDetectionAvailable();
    }

    @Override
    protected void onServiceConnected() {
        if (fingerprintGestureCallback != null
                || !mIsGestureDetectionAvailable) {
            return;
        }

        fingerprintGestureCallback =
               new FingerprintGestureController.FingerprintGestureCallback() {
            @Override
            public void onGestureDetected(int gesture) {
                switch (gesture) {
                    case FINGERPRINT_GESTURE_SWIPE_DOWN:
                        moveGameCursorDown();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_LEFT:
                        moveGameCursorLeft();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_RIGHT:
                        moveGameCursorRight();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_UP:
                        moveGameCursorUp();
                        break;
                    default:
                        Log.e(MY_APP_TAG,
                                  "Error: Unknown gesture type detected!");
                        break;
                }
            }

            @Override
            public void onGestureDetectionAvailabilityChanged(boolean available) {
                mIsGestureDetectionAvailable = available;
            }
        };

        if (fingerprintGestureCallback != null) {
            gestureController.registerFingerprintGestureCallback(
                    fingerprintGestureCallback, null);
        }
    }
}

מידע נוסף זמין בסרטון של ההרצאה What's new in Android accessibility מ-Google I/O 2017, החל מדקה 9:03.

המרת טקסט לדיבור בשפות שונות

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

Kotlin

val localeWrappedTextView = findViewById<TextView>(R.id.my_french_greeting_text).apply {
    text = wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE)
}

private fun wrapTextInLocaleSpan(originalText: CharSequence, loc: Locale): SpannableStringBuilder {
    return SpannableStringBuilder(originalText).apply {
        setSpan(LocaleSpan(loc), 0, originalText.length - 1, 0)
    }
}

Java

TextView localeWrappedTextView = findViewById(R.id.my_french_greeting_text);
localeWrappedTextView.setText(wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE));

private SpannableStringBuilder wrapTextInLocaleSpan(
        CharSequence originalText, Locale loc) {
    SpannableStringBuilder myLocaleBuilder =
            new SpannableStringBuilder(originalText);
    myLocaleBuilder.setSpan(new LocaleSpan(loc), 0,
            originalText.length() - 1, 0);
    return myLocaleBuilder;
}

למידע נוסף, אפשר לצפות בסרטון של הסשן What's new in Android accessibility (מה חדש בנגישות ב-Android) מ-Google I/O 2017, החל מדקה 10:59.

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

החל משנת 2011, שירותי נגישות יכולים לפעול בשם המשתמשים, כולל שינוי המיקוד של הקלט ובחירה (הפעלה) של רכיבי ממשק משתמש. בשנת 2012, טווח הפעולות הורחב וכלל גלילה ברשימות ואינטראקציה עם שדות טקסט. שירותי נגישות יכולים גם לבצע פעולות גלובליות, כמו ניווט למסך הבית, לחיצה על הכפתור 'הקודם' ופתיחה של מסך ההתראות ורשימת האפליקציות האחרונות. מאז 2012, מערכת Android כוללת הדגשה של נגישות, שמאפשרת לבחור את כל הרכיבים שמוצגים באמצעות שירות נגישות.

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

האזנה לתנועות

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

Kotlin

class MyAccessibilityService : AccessibilityService() {

    override fun onCreate() {
        serviceInfo.flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE
    }
    ...
}

Java

public class MyAccessibilityService extends AccessibilityService {
    @Override
    public void onCreate() {
        getServiceInfo().flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
    }
    ...
}

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

תנועות רציפות

מכשירים עם Android מגרסה 8.0 (רמת API‏ 26) ומעלה תומכים במחוות המשך, או במחוות מתוכנתות שמכילות יותר מאובייקט Path.

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

Kotlin

// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
private fun doRightThenDownDrag() {
    val dragRightPath = Path().apply {
        moveTo(200f, 200f)
        lineTo(400f, 200f)
    }
    val dragRightDuration = 500L // 0.5 second

    // The starting point of the second path must match
    // the ending point of the first path.
    val dragDownPath = Path().apply {
        moveTo(400f, 200f)
        lineTo(400f, 400f)
    }
    val dragDownDuration = 500L
    val rightThenDownDrag = GestureDescription.StrokeDescription(
            dragRightPath,
            0L,
            dragRightDuration,
            true
    ).apply {
        continueStroke(dragDownPath, dragRightDuration, dragDownDuration, false)
    }
}

Java

// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
private void doRightThenDownDrag() {
    Path dragRightPath = new Path();
    dragRightPath.moveTo(200, 200);
    dragRightPath.lineTo(400, 200);
    long dragRightDuration = 500L; // 0.5 second

    // The starting point of the second path must match
    // the ending point of the first path.
    Path dragDownPath = new Path();
    dragDownPath.moveTo(400, 200);
    dragDownPath.lineTo(400, 400);
    long dragDownDuration = 500L;
    GestureDescription.StrokeDescription rightThenDownDrag =
            new GestureDescription.StrokeDescription(dragRightPath, 0L,
            dragRightDuration, true);
    rightThenDownDrag.continueStroke(dragDownPath, dragRightDuration,
            dragDownDuration, false);
}

מידע נוסף זמין בסרטון של הסשן What's new in Android accessibility מ-Google I/O 2017, החל מדקה 15:47.

שימוש בפעולות של נגישות לבעלי מוגבלות

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

כדי לפעול בשם המשתמשים, שירות הנגישות צריך להירשם לקבלת אירועים מאפליקציות ולבקש הרשאה לצפייה בתוכן של אפליקציות על ידי הגדרת android:canRetrieveWindowContent ל-true בקובץ ההגדרות של השירות. כשהשירות מקבל אירועים, הוא יכול לאחזר את אובייקט AccessibilityNodeInfo מהאירוע באמצעות getSource(). בעזרת אובייקט AccessibilityNodeInfo, השירות יכול לבדוק את היררכיית התצוגה כדי לקבוע איזו פעולה לבצע, ואז לבצע את הפעולה בשם המשתמש באמצעות performAction().

Kotlin

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Get the source node of the event.
        event.source?.apply {

            // Use the event and node information to determine what action to
            // take.

            // Act on behalf of the user.
            performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)

            // Recycle the nodeInfo object.
            recycle()
        }
    }
    ...
}

Java

public class MyAccessibilityService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // Get the source node of the event.
        AccessibilityNodeInfo nodeInfo = event.getSource();

        // Use the event and node information to determine what action to take.

        // Act on behalf of the user.
        nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);

        // Recycle the nodeInfo object.
        nodeInfo.recycle();
    }
    ...
}

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

שימוש בסוגי מיקוד

בשנת 2012, הוסף ל-Android ממשק משתמש שנקרא הדגשת נגישות. שירותי נגישות יכולים להשתמש במיקוד הזה כדי לבחור כל רכיב גלוי בממשק המשתמש ולבצע בו פעולה. סוג ההתמקדות הזה שונה מהתמקדות בהזנת קלט, שקובעת איזה אלמנט בממשק המשתמש במסך מקבל קלט כשמשתמש מקליד תווים, לוחץ על Enter במקלדת או לוחץ על הלחצן המרכזי של לחצני החיצים (D-pad).

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

שירות נגישות יכול לקבוע לאיזה רכיב בממשק המשתמש יש מיקוד קלט או מיקוד נגישות באמצעות ה-method‏ AccessibilityNodeInfo.findFocus(). אפשר גם לחפש רכיבים שאפשר לבחור באמצעות מיקוד קלט באמצעות השיטה focusSearch(). לבסוף, שירות הנגישות יכול להגדיר את מיקום הפוקוס של הנגישות באמצעות ה-method‏ performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS).

איסוף מידע

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

קבלת פרטים על שינוי חלון

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

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

קבלת פרטי האירוע

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

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

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

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

  • AccessibilityEvent.getRecordCount() ו-getRecord(int): ה-methods האלה מאפשרות לכם לאחזר את קבוצת האובייקטים AccessibilityRecord שמשפיעים על AccessibilityEvent שמועבר אליכם על ידי המערכת. רמת הפירוט הזו מספקת הקשר נוסף לאירוע שמפעיל את שירות הנגישות.

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

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

android:canRetrieveWindowContent="true"

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

בדוגמה הבאה, הקוד מבצע את הפעולות הבאות כשמתקבל אירוע:

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

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

Kotlin

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

override fun onAccessibilityEvent(event: AccessibilityEvent) {

    val source: AccessibilityNodeInfo = event.source ?: return

    // Grab the parent of the view that fires the event.
    val rowNode: AccessibilityNodeInfo = getListItemNodeInfo(source) ?: return

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    val taskLabel: CharSequence = rowNode.getChild(0)?.text ?: run {
        rowNode.recycle()
        return
    }

    val isComplete: Boolean = rowNode.getChild(1)?.isChecked ?: run {
        rowNode.recycle()
        return
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.childCount < 2 || !rowNode.getChild(1).isCheckable) {
        rowNode.recycle()
        return
    }

    val completeStr: String = if (isComplete) {
        getString(R.string.checked)
    } else {
        getString(R.string.not_checked)
    }
    val reportStr = "$taskLabel$completeStr"
    speakToUser(reportStr)
}

Java

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {

    AccessibilityNodeInfo source = event.getSource();
    if (source == null) {
        return;
    }

    // Grab the parent of the view that fires the event.
    AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
    if (rowNode == null) {
        return;
    }

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    AccessibilityNodeInfo labelNode = rowNode.getChild(0);
    if (labelNode == null) {
        rowNode.recycle();
        return;
    }

    AccessibilityNodeInfo completeNode = rowNode.getChild(1);
    if (completeNode == null) {
        rowNode.recycle();
        return;
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
        rowNode.recycle();
        return;
    }

    CharSequence taskLabel = labelNode.getText();
    final boolean isComplete = completeNode.isChecked();
    String completeStr = null;

    if (isComplete) {
        completeStr = getString(R.string.checked);
    } else {
        completeStr = getString(R.string.not_checked);
    }
    String reportStr = taskLabel + completeStr;
    speakToUser(reportStr);
}

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

עיבוד טקסט

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

הסברים קצרים

ב-Android 9 (רמת API 28) נוספו כמה יכולות שמאפשרות לכם לגשת להסברים קצרים בממשק המשתמש של אפליקציה. משתמשים ב-getTooltipText() כדי לקרוא את הטקסט של תיאור כלי, וב-ACTION_SHOW_TOOLTIP וב-ACTION_HIDE_TOOLTIP כדי להנחות מופעים של View להציג או להסתיר את תיאורי הכלים שלהם.

טקסט של רמז

החל משנת 2017, מערכת Android כוללת כמה שיטות לאינטראקציה עם טקסט העזרה של אובייקט מבוסס-טקסט:

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

מיקומים של תווי טקסט במסך

במכשירים עם Android מגרסה 8.0 (רמת API‏ 26) ואילך, שירותי נגישות יכולים לקבוע את קואורדינטות המסך של תיבת התוחמת של כל תו גלוי בווידג'ט TextView. השירותים מוצאים את הקואורדינטות האלה על ידי קריאה ל-refreshWithExtraData(), העברת EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY כארגומנט הראשון ואובייקט Bundle כארגומנט השני. במהלך ההפעלה של השיטה, המערכת מאכלסת את הארגומנט Bundle במערך של אובייקטים מסוג Rect שאפשר להעביר בין תהליכים. כל אובייקט Rect מייצג את תיבת התוחמת של תו מסוים.

ערכים סטנדרטיים של טווח חד-צדדי

חלק מאובייקטים מסוג AccessibilityNodeInfo משתמשים במופע של AccessibilityNodeInfo.RangeInfo כדי לציין שרכיב בממשק המשתמש יכול לקבל טווח של ערכים. כשיוצרים טווח באמצעות RangeInfo.obtain(), או כשמאחזרים את ערכי הקצה של הטווח באמצעות getMin() ו-getMax(), חשוב לזכור שבמכשירים עם Android 8.0 (רמת API‏ 26) ומעלה, טווחים חד-צדדיים מיוצגים באופן סטנדרטי:

תגובה לאירועי נגישות

אחרי שהגדרתם את השירות כך שיפעל ויאזין לאירועים, צריך לכתוב קוד כדי שהשירות יידע מה לעשות כשמתקבל AccessibilityEvent. מתחילים בהחלפת השיטה onAccessibilityEvent(AccessibilityEvent). בשיטה הזו, משתמשים ב-getEventType() כדי לקבוע את סוג האירוע וב-getContentDescription() כדי לחלץ טקסט של תווית שמשויך לתצוגה שמפעילה את האירוע:

Kotlin

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    var eventText: String = when (event.eventType) {
        AccessibilityEvent.TYPE_VIEW_CLICKED -> "Clicked: "
        AccessibilityEvent.TYPE_VIEW_FOCUSED -> "Focused: "
        else -> ""
    }

    eventText += event.contentDescription

    // Do something nifty with this text, like speak the composed string back to
    // the user.
    speakToUser(eventText)
    ...
}

Java

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    final int eventType = event.getEventType();
    String eventText = null;
    switch(eventType) {
        case AccessibilityEvent.TYPE_VIEW_CLICKED:
            eventText = "Clicked: ";
            break;
        case AccessibilityEvent.TYPE_VIEW_FOCUSED:
            eventText = "Focused: ";
            break;
    }

    eventText = eventText + event.getContentDescription();

    // Do something nifty with this text, like speak the composed string back to
    // the user.
    speakToUser(eventText);
    ...
}

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

מידע נוסף זמין במקורות המידע הבאים:

מדריכים

Codelabs