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

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

במדריך הזה נסביר איך ליצור שירות נגישות ל-Android.

מחזור החיים של שירות נגישות

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

יצירת מחלקת השירות

יוצרים כיתה שמרחיבה את AccessibilityService. צריך לבטל את השיטות הבאות:

  • onAccessibilityEvent: מופעל כשהמערכת מזהה אירוע שתואם להגדרות של השירות (לדוגמה, שינוי המיקוד או לחיצה על לחצן). כאן השירות מפרש את ממשק המשתמש.
  • onInterrupt: מופעלת כשהמערכת מפריעה למשוב של השירות (לדוגמה, כדי להפסיק את פלט הדיבור כשהמשתמש מעביר את המיקוד במהירות).
package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.FingerprintGestureController
import android.accessibilityservice.AccessibilityButtonController
import android.accessibilityservice.GestureDescription
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.graphics.Path
import android.os.Build
import android.media.AudioManager
import android.content.Context

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Interpret the event and provide feedback to the user
    }

    override fun onInterrupt() {
        // Interrupt any ongoing feedback
    }

    override fun onServiceConnected() {
        // Perform initialization here
    }
}

הצהרה בקובץ המניפסט

רושמים את השירות בקובץ AndroidManifest.xml. אתם חייבים לאכוף את ההרשאה BIND_ACCESSIBILITY_SERVICE בצורה מחמירה, כך שרק המערכת תוכל לקשר את השירות שלכם.

כדי לוודא שכפתור ההגדרות פועל, צריך להצהיר על ServiceSettingsActivity.

<application>
  <service android:name=".accessibility.MyAccessibilityService"
      android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
      android:exported="true"
      android:label="@string/accessibility_service_label">
      <intent-filter>
          <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
      <meta-data
          android:name="android.accessibilityservice"
          android:resource="@xml/accessibility_service_config" />
  </service>

  <activity android:name=".accessibility.ServiceSettingsActivity"
      android:exported="true"
      android:label="@string/accessibility_service_settings_label" />
</application>

הגדרת השירות

יוצרים קובץ תצורה ב-res/xml/accessibility_service_config.xml. בקובץ הזה מוגדרים האירועים שהשירות מטפל בהם והמשוב שהוא מספק. חשוב להפנות אל ServiceSettingsActivity שהצהרתם עליו במניפסט:

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

קובץ התצורה כולל את המאפיינים העיקריים הבאים:

  • android:accessibilityEventTypes: האירועים שרוצים לקבל. שימוש ב-typeAllMask לשירות למטרות כלליות.
  • android:canRetrieveWindowContent: צריך להיות true אם השירות צריך לבדוק את היררכיית ממשק המשתמש (לדוגמה, כדי לקרוא טקסט מהמסך).
  • android:canPerformGestures: צריך להיות true אם אתם מתכוונים לשלוח מחוות (כמו החלקה או הקשה) באופן פרוגרמטי.
  • android:accessibilityFlags: אפשר לשלב מתגים כדי להפעיל תכונות. flagRequestFingerprintGestures נדרשת לתנועות של טביעות אצבעות. נדרשת הרשאה לflagRequestAccessibilityButton כדי להשתמש בלחצן הנגישות של התוכנה.

רשימה מלאה של אפשרויות ההגדרה מופיעה במאמר AccessibilityServiceInfo.

הגדרות זמן ריצה

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

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

override fun onServiceConnected() {
    val info = AccessibilityServiceInfo()

    // Set the type of events that this service wants to listen to.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

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

    // Set flags at runtime.
    info.flags = AccessibilityServiceInfo.FLAG_DEFAULT or
            AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES

    this.setServiceInfo(info)
}

ניתוח תוכן בממשק המשתמש

כש-onAccessibilityEvent() מופעל, המערכת מספקת AccessibilityEvent. האירוע הזה משמש כנקודת הכניסה לעץ הנגישות, שהוא ייצוג היררכי של תוכן המסך.

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

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

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    // Get the source node of the event
    val sourceNode: AccessibilityNodeInfo? = event.source

    if (sourceNode == null) return

    // Inspect properties
    if (sourceNode.isCheckable) {
        val state = if (sourceNode.isChecked) "Checked" else "Unchecked"
        val label = sourceNode.text ?: sourceNode.contentDescription
        
        // Provide feedback (for example, speak to the user)
        speakToUser("$label is $state")
    }

    // Always recycle nodes to prevent memory leaks
    sourceNode.recycle()
}

private fun speakToUser(text: String) {
    // Your text-to-speech implementation goes here
}

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

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

כדי לבצע פעולה, קוראים ל-performAction() באובייקט AccessibilityNodeInfo.

fun performClick(node: AccessibilityNodeInfo) {
    if (node.isClickable) {
        node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
    }
}

לפעולות גלובליות שמשפיעות על כל המערכת (כמו לחיצה על הכפתור "הקודם" או פתיחת מרכז ההתראות), משתמשים ב-performGlobalAction().

// Navigate back
fun navigateBack() {
    performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
}

ניהול המיקוד

ב-Android יש שני סוגים שונים של מיקוד: מיקוד קלט (המקום שאליו מועבר קלט מהמקלדת) ומיקוד נגישות (מה ששירות הנגישות בודק).

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

// Find the node that currently has accessibility focus
// Note: rootInActiveWindow can be null if the window is not available
val root = rootInActiveWindow
if (root != null) {
    val focusedNode = root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)

    // Do something with focusedNode

    // Always recycle nodes
    focusedNode?.recycle()
    // rootInActiveWindow doesn't need to be recycled, but obtained nodes do.
}

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

// Request that the system give focus to a given node
fun focusNode(node: AccessibilityNodeInfo) {
    node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)
}

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

ביצוע תנועות

השירות יכול לשלוח תנועות מותאמות אישית למסך, כמו החלקות, הקשות או אינטראקציות מרובות מגע. כדי לעשות זאת, צריך להצהיר על android:canPerformGestures="true" בהגדרה כדי שאפשר יהיה להשתמש ב-dispatchGesture() API.

תנועות פשוטות

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

fun swipeRight() {
    // Create a path for the swipe (from x=100 to x=500)
    val swipePath = Path()
    swipePath.moveTo(100f, 500f)
    swipePath.lineTo(500f, 500f)

    // Build the stroke description (0ms delay, 500ms duration)
    val stroke = GestureDescription.StrokeDescription(swipePath, 0, 500)

    // Build the gesture description
    val gestureBuilder = GestureDescription.Builder()
    gestureBuilder.addStroke(stroke)

    // Dispatch the gesture
    dispatchGesture(gestureBuilder.build(), object : AccessibilityService.GestureResultCallback() {
        override fun onCompleted(gestureDescription: GestureDescription?) {
            super.onCompleted(gestureDescription)
            // Gesture finished successfully
        }
    }, null)
}

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

באינטראקציות מורכבות (כמו ציור של צורת L או ביצוע גרירה מדויקת בכמה שלבים), אפשר לשרשר את המחוות באמצעות הפרמטר willContinue.

fun performLShapedGesture() {
    val path1 = Path().apply {
        moveTo(200f, 200f)
        lineTo(400f, 200f)
    }

    val path2 = Path().apply {
        moveTo(400f, 200f)
        lineTo(400f, 400f)
    }

    // First stroke: willContinue = true
    val stroke1 = GestureDescription.StrokeDescription(path1, 0, 500, true)

    // Second stroke: continues immediately after stroke1
    val stroke2 = stroke1.continueStroke(path2, 0, 500, false)

    val builder = GestureDescription.Builder()
    builder.addStroke(stroke1)
    builder.addStroke(stroke2)

    dispatchGesture(builder.build(), null, null)
}

ניהול אודיו

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

fun increaseAccessibilityVolume() {
    val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    audioManager.adjustStreamVolume(
        AudioManager.STREAM_ACCESSIBILITY,
        AudioManager.ADJUST_RAISE,
        0
    )
}

חשוב לכלול את הדגל FLAG_ENABLE_ACCESSIBILITY_VOLUME בהגדרה, ב-XML או באמצעות setServiceInfo בזמן הריצה.

תכונות מתקדמות

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

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

מוסיפים את הלוגיקה הבאה לשיטה onServiceConnected():

// Import: android.os.Build
// Import: android.accessibilityservice.FingerprintGestureController

private var gestureController: FingerprintGestureController? = null

override fun onServiceConnected() {
    // Check if the device is running Android 10 (Q) or higher
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        gestureController = fingerprintGestureController

        val callback = object : FingerprintGestureController.FingerprintGestureCallback() {
            override fun onGestureDetected(gesture: Int) {
                when (gesture) {
                    FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_DOWN -> {
                        // Handle swipe down
                    }
                    FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_UP -> {
                        // Handle swipe up
                    }
                }
            }
        }

        gestureController?.registerFingerprintGestureCallback(callback, null)
    }
}

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

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

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

// Import: android.accessibilityservice.AccessibilityButtonController

override fun onServiceConnected() {
    // ... existing initialization code ...

    val controller = accessibilityButtonController

    controller.registerAccessibilityButtonCallback(
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                // Respond to button tap
            }
        }
    )
}

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

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

import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.LocaleSpan
import java.util.Locale

// Wrap text in LocaleSpan to indicate language
val spannable = SpannableStringBuilder("Bonjour")
spannable.setSpan(
    LocaleSpan(Locale.FRANCE),
    0,
    spannable.length,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)

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

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

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

מדריכים

Codelabs

צפייה בתוכן