Tworzenie usługi ułatwień dostępu

Usługa ułatwień dostępu to aplikacja, która ulepsza interfejs użytkownika, aby pomagać osobom z niepełnosprawnościami lub osobom, które tymczasowo nie mogą w pełni korzystać z urządzenia. Te usługi działają w tle i komunikują się z systemem, aby sprawdzać zawartość ekranu i wchodzić w interakcje z aplikacjami w imieniu użytkownika. Przykładami są czytniki ekranu (np. TalkBack), narzędzia Switch Access i systemy sterowania głosem.

Ten przewodnik zawiera podstawowe informacje o tworzeniu usługi ułatwień dostępu na Androida.

Cykl życia usługi ułatwień dostępu

Aby utworzyć usługę ułatwień dostępu, musisz rozszerzyć klasę AccessibilityService i zadeklarować usługę w pliku manifestu aplikacji.

Tworzenie klasy usługi

Utwórz klasę, która rozszerza AccessibilityService. Musisz zastąpić te metody:

  • onAccessibilityEvent: wywoływana, gdy system wykryje zdarzenie, które pasuje do konfiguracji usługi (np. zmianę fokusu lub kliknięcie przycisku). W tym miejscu usługa interpretuje interfejs.
  • onInterrupt: wywoływana, gdy system przerywa działanie usługi (np. aby zatrzymać odpowiedź głosową, gdy użytkownik szybko przenosi fokus).
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
    }
}

Deklarowanie w pliku manifestu

Zarejestruj usługę w pliku AndroidManifest.xml. Musisz ściśle egzekwować uprawnienie BIND_ACCESSIBILITY_SERVICE, aby tylko system mógł powiązać się z Twoją usługą.

Aby przycisk ustawień działał, zadeklaruj 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>

Konfigurowanie usługi

Utwórz plik konfiguracji w folderze res/xml/accessibility_service_config.xml. Ten plik określa, jakie zdarzenia obsługuje Twoja usługa i jakie informacje zwrotne przekazuje. Odwołaj się do ServiceSettingsActivity zadeklarowanego w pliku manifestu:

<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" />

Plik konfiguracji zawiera te kluczowe atrybuty:

  • android:accessibilityEventTypes: zdarzenia, które chcesz otrzymywać. Użyj typeAllMask w przypadku usługi do zwykłych obciążeń.
  • android:canRetrieveWindowContent: musi mieć wartość true, jeśli usługa musi sprawdzać hierarchię interfejsu (np. aby odczytać tekst z ekranu).
  • android:canPerformGestures: musi mieć wartość true, jeśli zamierzasz programowo wysyłać gesty (np. przesuwanie lub klikanie).
  • android:accessibilityFlags: łącz flagi, aby włączyć funkcje. Do korzystania z gestów odciskiem palca wymagany jest program flagRequestFingerprintGestures. flagRequestAccessibilityButton jest wymagane w przypadku przycisku ułatwień dostępu do oprogramowania.

Pełną listę opcji konfiguracji znajdziesz w AccessibilityServiceInfo.

Konfiguracja środowiska wykonawczego

Konfiguracja XML jest statyczna, ale możesz też dynamicznie modyfikować konfigurację usługi w czasie działania. Jest to przydatne do przełączania funkcji na podstawie preferencji użytkownika.

Zastąp onServiceConnected(), aby zastosować aktualizacje środowiska wykonawczego za pomocą 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)
}

Interpretowanie treści interfejsu

Gdy onAccessibilityEvent() zostanie wywołany, system udostępni AccessibilityEvent. To zdarzenie jest punktem wejścia do drzewa ułatwień dostępu, czyli hierarchicznej reprezentacji treści na ekranie.

Twoja usługa wchodzi w interakcję głównie z AccessibilityNodeInfo obiektami, które reprezentują elementy interfejsu, takie jak przyciski, listy i tekst. Dane o tych elementach interfejsu są normalizowane do postaci AccessibilityNodeInfo.

Poniższy przykład pokazuje, jak pobrać źródło zdarzenia i przejść przez drzewo ułatwień dostępu, aby znaleźć informacje.

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
}

Działanie w imieniu użytkowników

Usługi ułatwień dostępu mogą wykonywać działania w imieniu użytkownika, np. klikać przyciski lub przewijać listy.

Aby wykonać działanie, wywołaj funkcję performAction() na obiekcie AccessibilityNodeInfo.

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

W przypadku działań globalnych, które mają wpływ na cały system (np. naciśnięcie przycisku Wstecz lub otwarcie obszaru powiadomień), użyj performGlobalAction().

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

Zarządzanie skupieniem

Android ma 2 rodzaje fokusu: fokus wejściowy (miejsce, do którego trafia wprowadzany tekst) i fokus ułatwień dostępu (element sprawdzany przez usługę ułatwień dostępu).

Poniższy fragment kodu pokazuje, jak znaleźć element, który ma obecnie fokus ułatwień dostępu:

// 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.
}

Poniższy fragment kodu pokazuje, jak przenieść fokus ułatwień dostępu na konkretny element:

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

Podczas tworzenia usługi ułatwień dostępu należy uwzględniać stan fokusu użytkownika i unikać przejmowania fokusu, chyba że zostało to wyraźnie wywołane przez działanie użytkownika.

Obsługa gestów

Usługa może wysyłać na ekran niestandardowe gesty, takie jak przesunięcia, kliknięcia czy interakcje wielodotykowe. Aby to zrobić, zadeklaruj android:canPerformGestures="true" w konfiguracji, aby móc używać interfejsu dispatchGesture() API.

Proste gesty

Aby wykonywać proste gesty, zacznij od utworzenia obiektu Path, który będzie reprezentować ruch związany z danym gestem. Następnie umieść PathGestureDescription, aby opisać pociągnięcie. Na koniec zadzwoń pod numer dispatchGesture, aby wysłać gest.

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)
}

Ciągłe gesty

W przypadku złożonych interakcji (takich jak narysowanie kształtu litery L lub wykonanie precyzyjnego wieloetapowego przeciągnięcia) możesz połączyć ze sobą pociągnięcia za pomocą parametru 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)
}

Zarządzanie dźwiękiem

Podczas tworzenia usługi ułatwień dostępu (zwłaszcza czytnika ekranu) używaj STREAM_ACCESSIBILITYstrumienia audio. Dzięki temu użytkownicy mogą sterować głośnością usługi niezależnie od głośności multimediów systemowych.

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

Pamiętaj, aby w konfiguracji uwzględnić flagę FLAG_ENABLE_ACCESSIBILITY_VOLUME – w pliku XML lub za pomocą setServiceInfo w czasie działania.

Funkcje zaawansowane

Gesty związane z odciskiem palca

Na urządzeniach z Androidem 10 (API na poziomie 29) lub nowszym usługa może rejestrować przesunięcia w określonym kierunku na czytniku linii papilarnych. Przydaje się to do udostępniania alternatywnych elementów sterujących nawigacją.

Dodaj do metody onServiceConnected() tę logikę:

// 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)
    }
}

Przycisk ułatwień dostępu

Na urządzeniach z programowymi klawiszami nawigacyjnymi użytkownicy mogą wywołać Twoją usługę za pomocą przycisku ułatwień dostępu na pasku nawigacyjnym.

Aby korzystać z tej funkcji, dodaj flagę FLAG_REQUEST_ACCESSIBILITY_BUTTON do konfiguracji usługi. Następnie dodaj logikę rejestracji do metody 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
            }
        }
    )
}

Wielojęzyczna zamiana tekstu na mowę

Usługa odczytująca tekst na głos może automatycznie przełączać języki, jeśli tekst źródłowy jest oznaczony tagiem LocaleSpan. Dzięki temu usługa może poprawnie odczytywać treści w różnych językach bez konieczności ręcznego przełączania.

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
)

Gdy usługa przetwarza AccessibilityNodeInfo, sprawdź właściwość text w przypadku obiektów LocaleSpan, aby określić prawidłowy język syntezy mowy.

Dodatkowe materiały

Więcej informacji znajdziesz w tych materiałach:

Przewodniki

Codelabs

Wyświetlanie treści