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