Membuat layanan aksesibilitas

Layanan aksesibilitas adalah aplikasi yang meningkatkan kualitas antarmuka pengguna untuk membantu pengguna difabel atau yang mungkin tidak dapat berinteraksi sepenuhnya dengan perangkat untuk sementara waktu. Layanan ini berjalan di latar belakang dan berkomunikasi dengan sistem untuk memeriksa konten layar dan berinteraksi dengan aplikasi atas nama pengguna. Contohnya mencakup pembaca layar (seperti TalkBack), alat Tombol Akses, dan sistem kontrol suara.

Panduan ini membahas dasar-dasar pembuatan layanan aksesibilitas Android.

Siklus proses layanan aksesibilitas

Untuk membuat layanan aksesibilitas, Anda harus memperluas class AccessibilityService dan mendeklarasikan layanan dalam manifes aplikasi Anda.

Buat class layanan

Buat class yang memperluas AccessibilityService. Anda harus mengganti metode berikut:

  • onAccessibilityEvent: Dipanggil saat sistem mendeteksi peristiwa yang cocok dengan konfigurasi layanan Anda (misalnya, perubahan fokus atau klik tombol). Di sinilah layanan Anda menafsirkan antarmuka pengguna.
  • onInterrupt: Dipanggil saat sistem mengganggu masukan layanan Anda (misalnya, untuk menghentikan output ucapan saat pengguna memindahkan fokus dengan cepat).
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
    }
}

Mendeklarasikan dalam manifes

Daftarkan layanan Anda di file AndroidManifest.xml. Anda harus menerapkan izin BIND_ACCESSIBILITY_SERVICE secara ketat sehingga hanya sistem yang dapat terikat ke layanan Anda.

Untuk memastikan tombol setelan berfungsi, deklarasikan 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>

Mengonfigurasi layanan

Buat file konfigurasi di res/xml/accessibility_service_config.xml. File ini menentukan peristiwa yang ditangani layanan Anda dan masukan yang diberikan. Pastikan untuk mereferensikan ServiceSettingsActivity yang Anda deklarasikan dalam manifes:

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

File konfigurasi mencakup atribut utama berikut:

  • android:accessibilityEventTypes: Peristiwa yang ingin Anda terima. Gunakan typeAllMask untuk layanan tujuan umum.
  • android:canRetrieveWindowContent: Harus true jika layanan Anda perlu memeriksa hierarki UI (misalnya, untuk membaca teks dari layar).
  • android:canPerformGestures: Harus true jika Anda ingin mengirim gestur (seperti geser atau ketuk) secara terprogram.
  • android:accessibilityFlags: Gabungkan tanda untuk mengaktifkan fitur. flagRequestFingerprintGestures diperlukan untuk gestur sidik jari. flagRequestAccessibilityButton diperlukan untuk tombol aksesibilitas software.

Untuk mengetahui daftar lengkap opsi konfigurasi, lihat AccessibilityServiceInfo.

Konfigurasi runtime

Meskipun konfigurasi XML bersifat statis, Anda juga dapat mengubah konfigurasi layanan secara dinamis saat runtime. Hal ini berguna untuk mengaktifkan/menonaktifkan fitur berdasarkan preferensi pengguna.

Ganti onServiceConnected() untuk menerapkan update runtime menggunakan 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)
}

Menafsirkan konten UI

Saat onAccessibilityEvent() dipicu, sistem akan memberikan AccessibilityEvent. Peristiwa ini berfungsi sebagai titik entri ke pohon aksesibilitas, representasi hierarkis dari konten layar.

Layanan Anda berinteraksi terutama dengan objek AccessibilityNodeInfo, yang merepresentasikan elemen UI seperti tombol, daftar, dan teks. Data tentang elemen UI ini dinormalisasi menjadi AccessibilityNodeInfo.

Contoh berikut menunjukkan cara mengambil sumber peristiwa dan menelusuri hierarki aksesibilitas untuk menemukan informasi.

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
}

Bertindak atas nama pengguna

Layanan aksesibilitas dapat melakukan tindakan, seperti mengklik tombol atau men-scroll daftar, atas nama pengguna.

Untuk melakukan tindakan, panggil performAction() pada objek AccessibilityNodeInfo.

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

Untuk tindakan global yang memengaruhi seluruh sistem (seperti menekan tombol Kembali atau membuka panel notifikasi), gunakan performGlobalAction().

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

Mengelola fokus

Android memiliki dua jenis fokus yang berbeda: fokus input (tempat input keyboard berada) dan fokus aksesibilitas (apa yang diperiksa oleh layanan aksesibilitas).

Cuplikan berikut menunjukkan cara menemukan elemen yang saat ini memiliki fokus aksesibilitas:

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

Cuplikan berikut menunjukkan cara memindahkan fokus aksesibilitas ke elemen tertentu:

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

Saat membuat layanan aksesibilitas, hormati status fokus pengguna dan hindari mengambil fokus kecuali dipicu secara eksplisit oleh tindakan pengguna.

Melakukan isyarat

Layanan Anda dapat mengirimkan gestur kustom ke layar, seperti geser, ketuk, atau interaksi multi-sentuh. Untuk melakukannya, deklarasikan android:canPerformGestures="true" dalam konfigurasi Anda sehingga Anda dapat menggunakan dispatchGesture() API.

Gestur sederhana

Untuk melakukan gestur sederhana, mulailah dengan membuat objek Path untuk merepresentasikan gerakan yang terkait dengan gestur tertentu. Kemudian, gabungkan Path dalam GestureDescription untuk mendeskripsikan goresan. Terakhir, panggil dispatchGesture untuk mengirimkan gestur.

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

Gestur berkelanjutan

Untuk interaksi yang kompleks (seperti menggambar bentuk L atau melakukan penarikan multi-langkah yang presisi), Anda dapat menggabungkan goresan menggunakan parameter 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)
}

Pengelolaan audio

Saat membuat layanan aksesibilitas (terutama pembaca layar), gunakan aliran audio STREAM_ACCESSIBILITY. Hal ini memungkinkan pengguna mengontrol volume layanan secara terpisah dari volume media sistem.

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

Pastikan untuk menyertakan tanda FLAG_ENABLE_ACCESSIBILITY_VOLUME dalam konfigurasi Anda, baik dalam XML maupun melalui setServiceInfo saat runtime.

Fitur lanjutan

Gestur sidik jari

Di perangkat yang menjalankan Android 10 (level API 29) atau yang lebih tinggi, layanan Anda dapat merekam gesekan terarah pada sensor sidik jari. Hal ini berguna untuk menyediakan kontrol navigasi alternatif.

Tambahkan logika berikut ke metode onServiceConnected() Anda:

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

Tombol aksesibilitas

Di perangkat yang menggunakan tombol navigasi software, pengguna dapat memanggil layanan Anda melalui tombol aksesibilitas di menu navigasi.

Untuk menggunakan fitur ini, tambahkan flag FLAG_REQUEST_ACCESSIBILITY_BUTTON ke konfigurasi layanan Anda. Kemudian, tambahkan logika pendaftaran ke metode onServiceConnected() Anda.

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

Text-to-speech multibahasa

Layanan yang membacakan teks dapat otomatis beralih bahasa jika teks sumber ditandai dengan LocaleSpan. Hal ini memungkinkan layanan Anda mengucapkan konten berbahasa campuran dengan benar tanpa perlu beralih bahasa secara manual.

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
)

Saat layanan Anda memproses AccessibilityNodeInfo, periksa properti text untuk objek LocaleSpan guna menentukan bahasa text-to-speech yang benar.

Referensi lainnya

Untuk mempelajari lebih lanjut, lihat referensi berikut:

Panduan

Codelab

Melihat konten