إنشاء خدمة مخصّصة لتسهيل الاستخدام

خدمة مخصّصة لتسهيل الاستخدام هي تطبيق يحسّن واجهة المستخدم لمساعدة المستخدمين ذوي الاحتياجات الخاصة أو الذين قد لا يتمكنون مؤقتًا من التفاعل بشكل كامل مع الجهاز. تعمل هذه الخدمات في الخلفية وتتواصل مع النظام لفحص محتوى الشاشة والتفاعل مع التطبيقات نيابةً عن المستخدم. وتشمل الأمثلة قارئات الشاشة (مثل 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().

الإيماءات البسيطة

لتنفيذ إيماءات بسيطة، ابدأ بإنشاء عنصر 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 في وقت التشغيل.

الميزات المتقدمة

إيماءات بصمات الإصبع

على الأجهزة التي تعمل بالإصدار 10 من نظام التشغيل Android (مستوى واجهة برمجة التطبيقات 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
            }
        }
    )
}

تحويل النص إلى كلام بلغات متعددة

يمكن لخدمة قراءة النص بصوت عالٍ أن تبدّل اللغات تلقائيًا إذا تم وضع علامة 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 لتحديد لغة تحويل النص إلى كلام الصحيحة.

مراجع إضافية

لمزيد من المعلومات، يُرجى الاطّلاع على المراجع التالية:

الأدلّة

الدروس التطبيقية حول الترميز

مشاهدة المحتوى