Создайте свою собственную службу доступности

Служба доступности — это приложение, которое расширяет пользовательский интерфейс, чтобы помочь пользователям с ограниченными возможностями или тем, кто временно не может полноценно взаимодействовать с устройством. Например, пользователям, которые находятся за рулем, ухаживают за маленьким ребенком или посещают очень шумную вечеринку, может потребоваться дополнительная или альтернативная обратная связь интерфейса.

Android предоставляет стандартные службы специальных возможностей, включая TalkBack , а разработчики могут создавать и распространять свои собственные службы. В этом документе объясняются основы создания службы доступности.

Службу специальных возможностей можно объединить с обычным приложением или создать как отдельный проект Android. Шаги по созданию службы одинаковы в любой ситуации.

Создайте свою службу доступности

В своем проекте создайте класс, расширяющий AccessibilityService :

Котлин

package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent

class MyAccessibilityService : AccessibilityService() {
...
    override fun onInterrupt() {}

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
...
}

Ява

package com.example.android.apis.accessibility;

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class MyAccessibilityService extends AccessibilityService {
...
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    }

    @Override
    public void onInterrupt() {
    }

...
}

Если вы создаете новый проект для этой Service и не планируете связывать с ним приложение, вы можете удалить стартовый класс Activity из своего источника.

Манифестные декларации и разрешения

Приложения, предоставляющие службы специальных возможностей, должны включать в свои манифесты определенные объявления, чтобы система Android воспринимала их как службу специальных возможностей. В этом разделе описаны обязательные и дополнительные настройки служб специальных возможностей.

Декларация службы доступности

Чтобы ваше приложение рассматривалось как служба специальных возможностей, включите элемент service , а не элемент activity , в элемент application в манифесте. Кроме того, в элемент service включите фильтр намерений службы доступности. Манифест также должен защищать службу, добавляя разрешение BIND_ACCESSIBILITY_SERVICE , чтобы гарантировать, что только система может привязаться к ней. Вот пример:

  <application>
    <service android:name=".MyAccessibilityService"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
        android:label="@string/accessibility_service_label">
      <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
    </service>
  </application>

Конфигурация службы доступности

Службы доступности должны предоставлять конфигурацию, определяющую типы событий доступности, которые обрабатывает служба, и дополнительную информацию о службе. Конфигурация службы доступности содержится в классе AccessibilityServiceInfo . Ваша служба может создать и установить конфигурацию, используя экземпляр этого класса и setServiceInfo() во время выполнения. Однако при использовании этого метода доступны не все параметры конфигурации.

Вы можете включить в свой манифест элемент <meta-data> со ссылкой на файл конфигурации, который позволит вам установить полный набор параметров для вашей службы специальных возможностей, как показано в следующем примере:

<service android:name=".MyAccessibilityService">
  ...
  <meta-data
    android:name="android.accessibilityservice"
    android:resource="@xml/accessibility_service_config" />
</service>

Этот элемент <meta-data> ссылается на XML-файл, который вы создаете в каталоге ресурсов вашего приложения: <project_dir>/res/xml/accessibility_service_config.xml> . В следующем коде показан пример содержимого файла конфигурации службы:

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

Дополнительные сведения об атрибутах XML, которые можно использовать в файле конфигурации службы доступности, см. в следующей справочной документации:

Дополнительные сведения о том, какие параметры конфигурации можно задавать динамически во время выполнения, см. в справочной документации AccessibilityServiceInfo .

Настройте службу доступности

При настройке переменных конфигурации для службы специальных возможностей учитывайте следующее, чтобы сообщить системе, как и когда запускаться:

  • На какие типы событий вы хотите, чтобы он реагировал?
  • Должна ли служба быть активной для всех приложений или только для определенных имен пакетов?
  • Какие типы обратной связи он использует?

У вас есть два варианта установки этих переменных. Вариант обратной совместимости — установить их в коде с помощью setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo) Для этого переопределите метод onServiceConnected() и настройте там свою службу, как показано в следующем примере:

Котлин

override fun onServiceConnected() {
    info.apply {
        // Set the type of events that this service wants to listen to. Others
        // aren't passed to this service.
        eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

        // If you only want this service to work with specific apps, set their
        // package names here. Otherwise, when the service is activated, it
        // listens to events from all apps.
        packageNames = arrayOf("com.example.android.myFirstApp", "com.example.android.mySecondApp")

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

        // Default services are invoked only if no package-specific services are
        // present for the type of AccessibilityEvent generated. This service is
        // app-specific, so the flag isn't necessary. For a general-purpose
        // service, consider setting the DEFAULT flag.

        // flags = AccessibilityServiceInfo.DEFAULT;

        notificationTimeout = 100
    }

    this.serviceInfo = info

}

Ява

@Override
public void onServiceConnected() {
    // Set the type of events that this service wants to listen to. Others
    // aren't passed to this service.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
            AccessibilityEvent.TYPE_VIEW_FOCUSED;

    // If you only want this service to work with specific apps, set their
    // package names here. Otherwise, when the service is activated, it listens
    // to events from all apps.
    info.packageNames = new String[]
            {"com.example.android.myFirstApp", "com.example.android.mySecondApp"};

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

    // Default services are invoked only if no package-specific services are
    // present for the type of AccessibilityEvent generated. This service is
    // app-specific, so the flag isn't necessary. For a general-purpose service,
    // consider setting the DEFAULT flag.

    // info.flags = AccessibilityServiceInfo.DEFAULT;

    info.notificationTimeout = 100;

    this.setServiceInfo(info);

}

Второй вариант — настроить службу с помощью XML-файла. Определенные параметры конфигурации, такие как canRetrieveWindowContent , доступны только в том случае, если вы настраиваете свою службу с использованием XML. Параметры конфигурации из предыдущего примера выглядят следующим образом, если они определены с использованием XML:

<accessibility-service
     android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
     android:packageNames="com.example.android.myFirstApp, com.example.android.mySecondApp"
     android:accessibilityFeedbackType="feedbackSpoken"
     android:notificationTimeout="100"
     android:settingsActivity="com.example.android.apis.accessibility.TestBackActivity"
     android:canRetrieveWindowContent="true"
/>

Если вы используете XML, укажите его в своем манифесте, добавив тег <meta-data> в объявление службы, указывающий на XML-файл. Если вы храните XML-файл в res/xml/serviceconfig.xml , новый тег будет выглядеть следующим образом:

<service android:name=".MyAccessibilityService">
     <intent-filter>
         <action android:name="android.accessibilityservice.AccessibilityService" />
     </intent-filter>
     <meta-data android:name="android.accessibilityservice"
     android:resource="@xml/serviceconfig" />
</service>

Методы службы доступности

Служба доступности должна расширить класс AccessibilityService и переопределить следующие методы этого класса. Эти методы представлены в том порядке, в котором система Android их вызывает: от момента запуска службы ( onServiceConnected() ) до момента ее работы ( onAccessibilityEvent() , onInterrupt() ) до момента ее закрытия ( onUnbind() ).

  • onServiceConnected() : (необязательно) система вызывает этот метод при подключении к вашей службе доступности. Используйте этот метод для выполнения единоразовых действий по настройке вашего сервиса, включая подключение к службам системы обратной связи с пользователем, таким как аудиоменеджер или вибратор устройства. Если вы хотите настроить конфигурацию вашего сервиса во время выполнения или внести единовременные изменения, это удобное место для вызова setServiceInfo() .

  • onAccessibilityEvent() : (обязательно) система вызывает этот метод обратно, когда обнаруживает AccessibilityEvent , соответствующий параметрам фильтрации событий, указанным вашей службой специальных возможностей, например, когда пользователь нажимает кнопку или фокусируется на элементе управления пользовательским интерфейсом в приложении, обеспечивающем доступ к вашим специальным возможностям. сервис предоставляет обратную связь. Когда система вызывает этот метод, она передает связанное AccessibilityEvent , которое служба затем может интерпретировать и использовать для предоставления обратной связи пользователю. Этот метод можно вызывать много раз в течение жизненного цикла вашего сервиса.

  • onInterrupt() : (обязательно) система вызывает этот метод, когда система хочет прервать обратную связь, которую предоставляет ваша служба, обычно в ответ на действие пользователя, такое как перемещение фокуса на другой элемент управления. Этот метод можно вызывать много раз в течение жизненного цикла вашего сервиса.

  • onUnbind() : (необязательно) система вызывает этот метод, когда система собирается завершить работу службы специальных возможностей. Используйте этот метод для выполнения любых однократных процедур выключения, включая отмену выделения системных служб обратной связи с пользователем, таких как диспетчер звука или вибратор устройства.

Эти методы обратного вызова обеспечивают базовую структуру вашей службы доступности. Вы можете решить, как обрабатывать данные, предоставленные системой Android в виде объектов AccessibilityEvent , и предоставлять обратную связь пользователю. Дополнительные сведения о получении информации из события доступности см. в разделе Получение сведений о событии .

Зарегистрируйтесь на мероприятия, посвященные доступности

Одна из наиболее важных функций параметров конфигурации службы доступности — позволить вам указать, какие типы событий доступности может обрабатывать ваша служба. Указание этой информации позволяет службам специальных возможностей взаимодействовать друг с другом и дает вам возможность обрабатывать только определенные типы событий из определенных приложений. Фильтрация событий может включать в себя следующие критерии:

  • Имена пакетов: укажите имена пакетов приложений, события доступности которых вы хотите, чтобы ваша служба обрабатывала. Если этот параметр опущен, ваша служба специальных возможностей считается доступной для обслуживания событий доступности для любого приложения. Вы можете установить этот параметр в файлах конфигурации службы доступности с помощью атрибута android:packageNames в виде списка, разделенного запятыми, или использовать член AccessibilityServiceInfo.packageNames .

  • Типы событий: укажите типы событий доступности, которые должна обрабатывать ваша служба. Вы можете установить этот параметр в файлах конфигурации службы специальных возможностей с помощью атрибута android:accessibilityEventTypes в виде списка, разделенного | символ, например accessibilityEventTypes="typeViewClicked|typeViewFocused" . Или вы можете установить его с помощью члена AccessibilityServiceInfo.eventTypes .

При настройке службы доступности внимательно продумайте, какие события может обрабатывать ваша служба, и регистрируйтесь только для этих событий. Поскольку пользователи могут одновременно активировать несколько служб специальных возможностей, ваша служба не должна использовать события, которые она не может обработать. Помните, что другие службы могут обрабатывать эти события, чтобы улучшить взаимодействие с пользователем.

Объем доступности

Устройства под управлением Android 8.0 (уровень API 26) и более поздних версий включают категорию громкости STREAM_ACCESSIBILITY , которая позволяет вам контролировать громкость аудиовыхода вашей службы доступности независимо от других звуков на устройстве.

Службы доступности могут использовать этот тип потока, установив параметр FLAG_ENABLE_ACCESSIBILITY_VOLUME . Затем вы можете изменить громкость звука специальных возможностей устройства, вызвав метод adjustStreamVolume() в экземпляре AudioManager устройства.

Следующий фрагмент кода демонстрирует, как служба специальных возможностей может использовать категорию тома STREAM_ACCESSIBILITY :

Котлин

import android.media.AudioManager.*

class MyAccessibilityService : AccessibilityService() {

    private val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager

    override fun onAccessibilityEvent(accessibilityEvent: AccessibilityEvent) {
        if (accessibilityEvent.source.text == "Increase volume") {
            audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY, ADJUST_RAISE, 0)
        }
    }
}

Ява

import static android.media.AudioManager.*;

public class MyAccessibilityService extends AccessibilityService {
    private AudioManager audioManager =
            (AudioManager) getSystemService(AUDIO_SERVICE);

    @Override
    public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
        AccessibilityNodeInfo interactedNodeInfo =
                accessibilityEvent.getSource();
        if (interactedNodeInfo.getText().equals("Increase volume")) {
            audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY,
                ADJUST_RAISE, 0);
        }
    }
}

Дополнительные сведения см. в видеосеансе «Что нового в специальных возможностях Android» с конференции Google I/O 2017, начиная с 6:35.

Ярлык специальных возможностей

На устройствах под управлением Android 8.0 (уровень API 26) и выше пользователи могут включать и отключать предпочитаемую службу специальных возможностей с любого экрана, одновременно нажимая и удерживая обе клавиши регулировки громкости. Хотя этот ярлык включает и отключает Talkback по умолчанию, пользователи могут настроить кнопку для включения и отключения любой службы, установленной на их устройстве.

Чтобы пользователи могли получить доступ к определенной службе специальных возможностей с помощью ярлыка специальных возможностей, служба должна запросить эту функцию во время выполнения.

Дополнительные сведения см. в видеосеансе «Что нового в специальных возможностях Android» с конференции Google I/O 2017, начиная с 13:25.

Кнопка специальных возможностей

На устройствах, использующих программную область навигации и работающих под управлением Android 8.0 (уровень API 26) или более поздней версии, в правой части панели навигации имеется кнопка специальных возможностей . Когда пользователи нажимают эту кнопку, они могут вызвать одну из нескольких включенных функций и служб специальных возможностей в зависимости от содержимого, отображаемого в данный момент на экране.

Чтобы пользователи могли вызывать данную службу специальных возможностей с помощью кнопки специальных возможностей, службе необходимо добавить флаг FLAG_REQUEST_ACCESSIBILITY_BUTTON в атрибут android:accessibilityFlags объекта AccessibilityServiceInfo . Затем служба может зарегистрировать обратные вызовы с помощью registerAccessibilityButtonCallback() .

В следующем фрагменте кода показано, как можно настроить службу специальных возможностей для реагирования на нажатие пользователем кнопки специальных возможностей:

Котлин

private var mAccessibilityButtonController: AccessibilityButtonController? = null
private var accessibilityButtonCallback:
        AccessibilityButtonController.AccessibilityButtonCallback? = null
private var mIsAccessibilityButtonAvailable: Boolean = false

override fun onServiceConnected() {
    mAccessibilityButtonController = accessibilityButtonController
    mIsAccessibilityButtonAvailable =
            mAccessibilityButtonController?.isAccessibilityButtonAvailable ?: false

    if (!mIsAccessibilityButtonAvailable) return

    serviceInfo = serviceInfo.apply {
        flags = flags or AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON
    }

    accessibilityButtonCallback =
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!")

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            override fun onAvailabilityChanged(
                    controller: AccessibilityButtonController,
                    available: Boolean
            ) {
                if (controller == mAccessibilityButtonController) {
                    mIsAccessibilityButtonAvailable = available
                }
            }
    }

    accessibilityButtonCallback?.also {
        mAccessibilityButtonController?.registerAccessibilityButtonCallback(it, null)
    }
}

Ява

private AccessibilityButtonController accessibilityButtonController;
private AccessibilityButtonController
        .AccessibilityButtonCallback accessibilityButtonCallback;
private boolean mIsAccessibilityButtonAvailable;

@Override
protected void onServiceConnected() {
    accessibilityButtonController = getAccessibilityButtonController();
    mIsAccessibilityButtonAvailable =
            accessibilityButtonController.isAccessibilityButtonAvailable();

    if (!mIsAccessibilityButtonAvailable) {
        return;
    }

    AccessibilityServiceInfo serviceInfo = getServiceInfo();
    serviceInfo.flags
            |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
    setServiceInfo(serviceInfo);

    accessibilityButtonCallback =
        new AccessibilityButtonController.AccessibilityButtonCallback() {
            @Override
            public void onClicked(AccessibilityButtonController controller) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!");

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            @Override
            public void onAvailabilityChanged(
              AccessibilityButtonController controller, boolean available) {
                if (controller.equals(accessibilityButtonController)) {
                    mIsAccessibilityButtonAvailable = available;
                }
            }
        };

    if (accessibilityButtonCallback != null) {
        accessibilityButtonController.registerAccessibilityButtonCallback(
                accessibilityButtonCallback, null);
    }
}

Дополнительные сведения см. в видеосеансе «Что нового в специальных возможностях Android» с конференции Google I/O 2017, начиная с 16:28.

Жесты отпечатков пальцев

Службы специальных возможностей на устройствах под управлением Android 8.0 (уровень API 26) или выше могут реагировать на направленные движения (вверх, вниз, влево и вправо) по датчику отпечатков пальцев устройства. Чтобы настроить службу для получения обратных вызовов об этих взаимодействиях, выполните следующую последовательность действий:

  1. Объявите разрешение USE_BIOMETRIC и возможность CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES .
  2. Установите флаг FLAG_REQUEST_FINGERPRINT_GESTURES в атрибуте android:accessibilityFlags .
  3. Зарегистрируйтесь для обратных вызовов, используя registerFingerprintGestureCallback() .

Имейте в виду, что не все устройства оснащены датчиками отпечатков пальцев. Чтобы определить, поддерживает ли устройство датчик, используйте метод isHardwareDetected() . Даже на устройстве, оснащенном датчиком отпечатков пальцев, ваша служба не сможет использовать этот датчик, когда он используется для целей аутентификации. Чтобы определить, когда датчик доступен, вызовите метод isGestureDetectionAvailable() и реализуйте обратный вызов onGestureDetectionAvailabilityChanged() .

В следующем фрагменте кода показан пример использования жестов отпечатков пальцев для навигации по виртуальному игровому полю:

// AndroidManifest.xml
<manifest ... >
    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
    ...
    <application>
        <service android:name="com.example.MyFingerprintGestureService" ... >
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/myfingerprintgestureservice" />
        </service>
    </application>
</manifest>
// myfingerprintgestureservice.xml
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:accessibilityFlags=" ... |flagRequestFingerprintGestures"
    android:canRequestFingerprintGestures="true"
    ... />

Котлин

// MyFingerprintGestureService.kt
import android.accessibilityservice.FingerprintGestureController.*

class MyFingerprintGestureService : AccessibilityService() {

    private var gestureController: FingerprintGestureController? = null
    private var fingerprintGestureCallback:
            FingerprintGestureController.FingerprintGestureCallback? = null
    private var mIsGestureDetectionAvailable: Boolean = false

    override fun onCreate() {
        gestureController = fingerprintGestureController
        mIsGestureDetectionAvailable = gestureController?.isGestureDetectionAvailable ?: false
    }

    override fun onServiceConnected() {
        if (mFingerprintGestureCallback != null || !mIsGestureDetectionAvailable) return

        fingerprintGestureCallback =
                object : FingerprintGestureController.FingerprintGestureCallback() {
                    override fun onGestureDetected(gesture: Int) {
                        when (gesture) {
                            FINGERPRINT_GESTURE_SWIPE_DOWN -> moveGameCursorDown()
                            FINGERPRINT_GESTURE_SWIPE_LEFT -> moveGameCursorLeft()
                            FINGERPRINT_GESTURE_SWIPE_RIGHT -> moveGameCursorRight()
                            FINGERPRINT_GESTURE_SWIPE_UP -> moveGameCursorUp()
                            else -> Log.e(MY_APP_TAG, "Error: Unknown gesture type detected!")
                        }
                    }

                    override fun onGestureDetectionAvailabilityChanged(available: Boolean) {
                        mIsGestureDetectionAvailable = available
                    }
                }

        fingerprintGestureCallback?.also {
            gestureController?.registerFingerprintGestureCallback(it, null)
        }
    }
}

Ява

// MyFingerprintGestureService.java
import static android.accessibilityservice.FingerprintGestureController.*;

public class MyFingerprintGestureService extends AccessibilityService {
    private FingerprintGestureController gestureController;
    private FingerprintGestureController
            .FingerprintGestureCallback fingerprintGestureCallback;
    private boolean mIsGestureDetectionAvailable;

    @Override
    public void onCreate() {
        gestureController = getFingerprintGestureController();
        mIsGestureDetectionAvailable =
                gestureController.isGestureDetectionAvailable();
    }

    @Override
    protected void onServiceConnected() {
        if (fingerprintGestureCallback != null
                || !mIsGestureDetectionAvailable) {
            return;
        }

        fingerprintGestureCallback =
               new FingerprintGestureController.FingerprintGestureCallback() {
            @Override
            public void onGestureDetected(int gesture) {
                switch (gesture) {
                    case FINGERPRINT_GESTURE_SWIPE_DOWN:
                        moveGameCursorDown();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_LEFT:
                        moveGameCursorLeft();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_RIGHT:
                        moveGameCursorRight();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_UP:
                        moveGameCursorUp();
                        break;
                    default:
                        Log.e(MY_APP_TAG,
                                  "Error: Unknown gesture type detected!");
                        break;
                }
            }

            @Override
            public void onGestureDetectionAvailabilityChanged(boolean available) {
                mIsGestureDetectionAvailable = available;
            }
        };

        if (fingerprintGestureCallback != null) {
            gestureController.registerFingerprintGestureCallback(
                    fingerprintGestureCallback, null);
        }
    }
}

Дополнительные сведения см. в видеосеансе «Что нового в специальных возможностях Android» с конференции Google I/O 2017, начиная с 9:03.

Многоязычный текст в речь

Начиная с Android 8.0 (уровень API 26), служба преобразования текста в речь (TTS) Android может идентифицировать и произносить фразы на нескольких языках в одном блоке текста. Чтобы включить эту возможность автоматического переключения языка в службе специальных возможностей, оберните все строки в объекты LocaleSpan , как показано в следующем фрагменте кода:

Котлин

val localeWrappedTextView = findViewById<TextView>(R.id.my_french_greeting_text).apply {
    text = wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE)
}

private fun wrapTextInLocaleSpan(originalText: CharSequence, loc: Locale): SpannableStringBuilder {
    return SpannableStringBuilder(originalText).apply {
        setSpan(LocaleSpan(loc), 0, originalText.length - 1, 0)
    }
}

Ява

TextView localeWrappedTextView = findViewById(R.id.my_french_greeting_text);
localeWrappedTextView.setText(wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE));

private SpannableStringBuilder wrapTextInLocaleSpan(
        CharSequence originalText, Locale loc) {
    SpannableStringBuilder myLocaleBuilder =
            new SpannableStringBuilder(originalText);
    myLocaleBuilder.setSpan(new LocaleSpan(loc), 0,
            originalText.length() - 1, 0);
    return myLocaleBuilder;
}

Дополнительные сведения см. в видеосеансе «Что нового в специальных возможностях Android» с конференции Google I/O 2017, начиная с 10:59.

Действовать от имени пользователей

Начиная с 2011 года сервисы доступности могут действовать от имени пользователей, включая изменение фокуса ввода и выбор (активацию) элементов пользовательского интерфейса. В 2012 году набор действий расширился за счет прокрутки списков и взаимодействия с текстовыми полями. Службы специальных возможностей также могут выполнять глобальные действия, такие как переход на главный экран, нажатие кнопки «Назад», открытие экрана уведомлений и списка последних приложений. С 2012 года Android включает фокус доступности , который позволяет выбирать все видимые элементы с помощью службы специальных возможностей.

Эти возможности позволяют разработчикам служб специальных возможностей создавать альтернативные режимы навигации, такие как навигация с помощью жестов, и предоставлять пользователям с ограниченными возможностями улучшенный контроль над своими устройствами на базе Android.

Слушайте жесты

Службы доступности могут прослушивать определенные жесты и реагировать, действуя от имени пользователя. Для использования этой функции требуется, чтобы ваша служба специальных возможностей запросила активацию функции «Изучение касанием». Ваша служба может запросить эту активацию, установив для члена flags экземпляра AccessibilityServiceInfo службы значение FLAG_REQUEST_TOUCH_EXPLORATION_MODE , как показано в следующем примере.

Котлин

class MyAccessibilityService : AccessibilityService() {

    override fun onCreate() {
        serviceInfo.flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE
    }
    ...
}

Ява

public class MyAccessibilityService extends AccessibilityService {
    @Override
    public void onCreate() {
        getServiceInfo().flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
    }
    ...
}

После того как ваша служба запросит активацию Explore by Touch, пользователь должен разрешить включение этой функции, если она еще не активна. Когда эта функция активна, ваша служба получает уведомление о жестах доступности через метод обратного вызова onGesture() вашей службы и может ответить, действуя от имени пользователя.

Продолжение жестов

Устройства под управлением Android 8.0 (уровень API 26) поддерживают непрерывные жесты или программные жесты, содержащие более одного объекта Path .

При указании последовательности штрихов вы можете указать, что они принадлежат одному и тому же программному жесту, используя последний аргумент willContinue в конструкторе GestureDescription.StrokeDescription , как показано в следующем фрагменте кода:

Котлин

// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
private fun doRightThenDownDrag() {
    val dragRightPath = Path().apply {
        moveTo(200f, 200f)
        lineTo(400f, 200f)
    }
    val dragRightDuration = 500L // 0.5 second

    // The starting point of the second path must match
    // the ending point of the first path.
    val dragDownPath = Path().apply {
        moveTo(400f, 200f)
        lineTo(400f, 400f)
    }
    val dragDownDuration = 500L
    val rightThenDownDrag = GestureDescription.StrokeDescription(
            dragRightPath,
            0L,
            dragRightDuration,
            true
    ).apply {
        continueStroke(dragDownPath, dragRightDuration, dragDownDuration, false)
    }
}

Ява

// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
private void doRightThenDownDrag() {
    Path dragRightPath = new Path();
    dragRightPath.moveTo(200, 200);
    dragRightPath.lineTo(400, 200);
    long dragRightDuration = 500L; // 0.5 second

    // The starting point of the second path must match
    // the ending point of the first path.
    Path dragDownPath = new Path();
    dragDownPath.moveTo(400, 200);
    dragDownPath.lineTo(400, 400);
    long dragDownDuration = 500L;
    GestureDescription.StrokeDescription rightThenDownDrag =
            new GestureDescription.StrokeDescription(dragRightPath, 0L,
            dragRightDuration, true);
    rightThenDownDrag.continueStroke(dragDownPath, dragRightDuration,
            dragDownDuration, false);
}

Дополнительные сведения см. в видеосеансе «Что нового в специальных возможностях Android» с конференции Google I/O 2017, начиная с 15:47.

Используйте действия по обеспечению доступности

Службы специальных возможностей могут действовать от имени пользователей, упрощая взаимодействие с приложениями и повышая продуктивность. Возможность служб доступности выполнять действия была добавлена ​​в 2011 году и значительно расширена в 2012 году.

Чтобы действовать от имени пользователей, ваша служба специальных возможностей должна зарегистрироваться , чтобы получать события от приложений и запрашивать разрешение на просмотр содержимого приложений, установив android:canRetrieveWindowContent значение true в файле конфигурации службы . Когда ваша служба получает события, она может получить объект AccessibilityNodeInfo из события с помощью getSource() . С помощью объекта AccessibilityNodeInfo ваша служба может затем изучить иерархию представлений, чтобы определить, какое действие следует предпринять, а затем действовать от имени пользователя с помощью performAction() .

Котлин

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Get the source node of the event.
        event.source?.apply {

            // Use the event and node information to determine what action to
            // take.

            // Act on behalf of the user.
            performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)

            // Recycle the nodeInfo object.
            recycle()
        }
    }
    ...
}

Ява

public class MyAccessibilityService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // Get the source node of the event.
        AccessibilityNodeInfo nodeInfo = event.getSource();

        // Use the event and node information to determine what action to take.

        // Act on behalf of the user.
        nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);

        // Recycle the nodeInfo object.
        nodeInfo.recycle();
    }
    ...
}

Метод performAction() позволяет вашей службе выполнять действия внутри приложения. Если вашей службе необходимо выполнить глобальное действие, например перейти на главный экран, нажать кнопку «Назад» или открыть экран уведомлений или список последних приложений, используйте метод performGlobalAction() .

Используйте типы фокуса

В 2012 году Android представил фокус пользовательского интерфейса, названный фокусом доступности . Службы доступности могут использовать этот фокус для выбора любого видимого элемента пользовательского интерфейса и выполнения действий с ним. Этот тип фокуса отличается от фокуса ввода , который определяет, какой элемент экранного пользовательского интерфейса получает ввод, когда пользователь вводит символы, нажимает Enter на клавиатуре или нажимает центральную кнопку D-pad.

Один элемент пользовательского интерфейса может иметь фокус ввода, а другой элемент — фокус доступности. Цель фокуса на доступности — предоставить службам доступности метод взаимодействия с видимыми элементами на экране, независимо от того, является ли элемент фокусируемым на вводе с точки зрения системы. Чтобы убедиться, что ваша служба специальных возможностей правильно взаимодействует с элементами ввода приложений, следуйте рекомендациям по тестированию специальных возможностей приложения , чтобы протестировать свою службу при использовании типичного приложения.

Служба доступности может определить, какой элемент пользовательского интерфейса имеет фокус ввода или фокус доступности, используя метод AccessibilityNodeInfo.findFocus() . Вы также можете искать элементы, которые можно выбрать с помощью фокуса ввода, используя метод focusSearch() . Наконец, ваша служба доступности может установить фокус доступности с помощью метода performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS) .

Соберите информацию

Службы доступности имеют стандартные методы сбора и представления ключевых единиц предоставленной пользователем информации, таких как сведения о событиях, текст и числа.

Получить подробную информацию об изменении окна

Android 9 (уровень API 28) и выше позволяет приложениям отслеживать обновления окон, когда приложение перерисовывает несколько окон одновременно. Когда происходит событие TYPE_WINDOWS_CHANGED , используйте API getWindowChanges() , чтобы определить, как изменяются окна. Во время многооконного обновления каждое окно создает свой собственный набор событий. Метод getSource() возвращает корневое представление окна, связанного с каждым событием.

Если приложение определяет заголовки панели специальных возможностей для своих объектов View , ваша служба может распознавать обновление пользовательского интерфейса приложения. Когда происходит событие TYPE_WINDOW_STATE_CHANGED , используйте типы, возвращаемые функцией getContentChangeTypes() , чтобы определить, как изменяется окно. Например, платформа может определять, когда у панели появляется новый заголовок или когда панель исчезает.

Получить подробную информацию о мероприятии

Android предоставляет службам специальных возможностей информацию о взаимодействии пользовательского интерфейса через объекты AccessibilityEvent . В предыдущих версиях Android информация, доступная в событии доступности, хотя и предоставляла важные сведения об управлении пользовательским интерфейсом, выбранном пользователями, предлагала ограниченную контекстную информацию. Во многих случаях эта недостающая контекстная информация может иметь решающее значение для понимания значения выбранного элемента управления.

Примером интерфейса, где контекст имеет решающее значение, является календарь или ежедневник. Если пользователь выбирает временной интервал 16:00 в списке дней с понедельника по пятницу, а служба специальных возможностей объявляет «16:00», но не объявляет название дня недели, день месяца или название месяца, результирующий обратная связь сбивает с толку. В этом случае контекст элемента управления пользовательским интерфейсом имеет решающее значение для пользователя, который хочет запланировать встречу.

С 2011 года Android значительно расширяет объем информации, которую служба специальных возможностей может получить о взаимодействии с пользовательским интерфейсом, создавая события доступности на основе иерархии представлений. Иерархия представлений — это набор компонентов пользовательского интерфейса, которые содержат компонент (его родительские элементы) и элементы пользовательского интерфейса, которые могут содержаться в этом компоненте (его дочерние элементы). Таким образом, Android может предоставлять более подробную информацию о событиях доступности, позволяя службам доступности предоставлять пользователям более полезную обратную связь.

Служба доступности получает информацию о событии пользовательского интерфейса через событие AccessibilityEvent передаваемое системой в метод обратного вызова службы onAccessibilityEvent() . Этот объект предоставляет подробную информацию о событии, включая тип объекта, над которым выполняется действие, его описательный текст и другие сведения.

  • AccessibilityEvent.getRecordCount() и getRecord(int) : эти методы позволяют получить набор объектов AccessibilityRecord , которые вносят вклад в AccessibilityEvent , переданное вам системой. Этот уровень детализации предоставляет больше контекста для события, которое запускает вашу службу доступности.

  • AccessibilityRecord.getSource() : этот метод возвращает объект AccessibilityNodeInfo . Этот объект позволяет вам запросить иерархию макета представления (родительские и дочерние элементы) компонента, который создает событие доступности. Эта функция позволяет службе доступности исследовать полный контекст события, включая содержимое и состояние любых включающих или дочерних представлений.

Платформа Android предоставляет AccessibilityService возможность запрашивать иерархию представлений, собирая информацию о компоненте пользовательского интерфейса, который генерирует событие, а также о его родительском и дочернем элементах. Для этого установите следующую строку в вашей конфигурации XML:

android:canRetrieveWindowContent="true"

После этого получите объект AccessibilityNodeInfo с помощью getSource() . Этот вызов возвращает объект только в том случае, если окно, в котором возникает событие, все еще является активным окном. Если нет, он возвращает ноль, поэтому ведите себя соответственно.

В следующем примере код выполняет следующие действия при получении события:

  1. Немедленно захватывает родительский элемент представления, в котором происходит событие.
  2. В этом представлении ищет метку и флажок как дочерние представления.
  3. Если он их находит, создает строку для отчета пользователю, указывающую метку и то, была ли она проверена.

Если в какой-то момент при обходе иерархии представлений возвращается нулевое значение, метод незаметно прекращает работу.

Котлин

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

override fun onAccessibilityEvent(event: AccessibilityEvent) {

    val source: AccessibilityNodeInfo = event.source ?: return

    // Grab the parent of the view that fires the event.
    val rowNode: AccessibilityNodeInfo = getListItemNodeInfo(source) ?: return

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    val taskLabel: CharSequence = rowNode.getChild(0)?.text ?: run {
        rowNode.recycle()
        return
    }

    val isComplete: Boolean = rowNode.getChild(1)?.isChecked ?: run {
        rowNode.recycle()
        return
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.childCount < 2 || !rowNode.getChild(1).isCheckable) {
        rowNode.recycle()
        return
    }

    val completeStr: String = if (isComplete) {
        getString(R.string.checked)
    } else {
        getString(R.string.not_checked)
    }
    val reportStr = "$taskLabel$completeStr"
    speakToUser(reportStr)
}

Ява

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {

    AccessibilityNodeInfo source = event.getSource();
    if (source == null) {
        return;
    }

    // Grab the parent of the view that fires the event.
    AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
    if (rowNode == null) {
        return;
    }

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    AccessibilityNodeInfo labelNode = rowNode.getChild(0);
    if (labelNode == null) {
        rowNode.recycle();
        return;
    }

    AccessibilityNodeInfo completeNode = rowNode.getChild(1);
    if (completeNode == null) {
        rowNode.recycle();
        return;
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
        rowNode.recycle();
        return;
    }

    CharSequence taskLabel = labelNode.getText();
    final boolean isComplete = completeNode.isChecked();
    String completeStr = null;

    if (isComplete) {
        completeStr = getString(R.string.checked);
    } else {
        completeStr = getString(R.string.not_checked);
    }
    String reportStr = taskLabel + completeStr;
    speakToUser(reportStr);
}

Теперь у вас есть полноценная функционирующая служба доступности. Попробуйте настроить способ взаимодействия с пользователем, добавив механизм преобразования текста в речь Android или используя Vibrator для обеспечения тактильной обратной связи.

Текст процесса

Устройства под управлением Android 8.0 (уровень API 26) и более поздних версий включают несколько функций обработки текста, которые упрощают службам специальных возможностей идентификацию и обработку определенных блоков текста, которые появляются на экране.

Подсказки

В Android 9 (уровень API 28) представлено несколько возможностей, которые дают вам доступ к всплывающим подсказкам в пользовательском интерфейсе приложения. Используйте getTooltipText() , чтобы прочитать текст всплывающей подсказки, и используйте ACTION_SHOW_TOOLTIP и ACTION_HIDE_TOOLTIP , чтобы указать экземплярам View показывать или скрывать свои всплывающие подсказки.

Текст подсказки

Начиная с 2017 года в Android реализовано несколько методов взаимодействия с текстом подсказки текстового объекта:

  • Методы isShowingHintText() и setShowingHintText() указывают и устанавливают соответственно, представляет ли текущее текстовое содержимое узла текст подсказки узла.
  • getHintText() предоставляет доступ к самому тексту подсказки. Даже если объект не отображает текст подсказки, вызов getHintText() завершается успешно.

Расположение текстовых символов на экране

На устройствах под управлением Android 8.0 (уровень API 26) и выше службы специальных возможностей могут определять координаты экрана для ограничивающей рамки каждого видимого символа в виджете TextView . Службы находят эти координаты, вызывая refreshWithExtraData() , передавая EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY в качестве первого аргумента и объект Bundle в качестве второго аргумента. Во время выполнения метода система заполняет аргумент Bundle разделяемым массивом объектов Rect . Каждый объект Rect представляет собой ограничивающую рамку определенного символа.

Стандартизированные односторонние значения диапазона

Некоторые объекты AccessibilityNodeInfo используют экземпляр AccessibilityNodeInfo.RangeInfo , чтобы указать, что элемент пользовательского интерфейса может принимать диапазон значений. При создании диапазона с помощью RangeInfo.obtain() или при получении крайних значений диапазона с помощью getMin() и getMax() имейте в виду, что устройства под управлением Android 8.0 (уровень API 26) и выше представляют односторонние диапазоны в стандартизированным способом:

  • Для диапазонов без минимума Float.NEGATIVE_INFINITY представляет минимальное значение.
  • Для диапазонов без максимума Float.POSITIVE_INFINITY представляет максимальное значение.

Реагировать на события доступности

Теперь, когда ваша служба настроена для запуска и прослушивания событий, напишите код, чтобы она знала, что делать при поступлении AccessibilityEvent . Начните с переопределения метода onAccessibilityEvent(AccessibilityEvent) . В этом методе используйте getEventType() , чтобы определить тип события, и getContentDescription() , чтобы извлечь любой текст метки, связанный с представлением, которое запускает событие:

Котлин

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    var eventText: String = when (event.eventType) {
        AccessibilityEvent.TYPE_VIEW_CLICKED -> "Clicked: "
        AccessibilityEvent.TYPE_VIEW_FOCUSED -> "Focused: "
        else -> ""
    }

    eventText += event.contentDescription

    // Do something nifty with this text, like speak the composed string back to
    // the user.
    speakToUser(eventText)
    ...
}

Ява

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    final int eventType = event.getEventType();
    String eventText = null;
    switch(eventType) {
        case AccessibilityEvent.TYPE_VIEW_CLICKED:
            eventText = "Clicked: ";
            break;
        case AccessibilityEvent.TYPE_VIEW_FOCUSED:
            eventText = "Focused: ";
            break;
    }

    eventText = eventText + event.getContentDescription();

    // Do something nifty with this text, like speak the composed string back to
    // the user.
    speakToUser(eventText);
    ...
}

Дополнительные ресурсы

Чтобы узнать больше, посетите следующие ресурсы:

Путеводители

Кодлабы