无障碍服务是一个应用程序,它给有残疾的用户或暂时无法与设备完全交互的用户提供了更好的无障碍用户交互功能。比如驾驶、照顾小孩或者在吵闹的派对上可能需要额外或者替代的交互反馈。
Android提供了标准的无障碍服务,包括TalkBack,开发人员可以创建和发布自己的无障碍服务。
Android从1.6(API 4)开始引入了构建和部署无障碍服务的能力,并在Android 4.0(API 14)进行了重大改进。Android Support Library在Android 4.0版本上增加了支持增强无障碍服务功能,这样就能够兼容到Android 1.6。Android鼓励开发者使用Support Library来广泛兼容无障碍服务,并针对Android 4.0中引入的更高级的无障碍服务功能进行开发。
清单声明和权限
提供无障碍服务的应用程序必须在其应用程序清单中包含特定声明,以便被Android系统视为无障碍服务。
无障碍服务声明
为了是应用程序的无障碍服务能够正常使用,必须在应用程序清单中application元素中包含一个service元素。另外,在service元素中,还必须包含无障碍服务的intent filter。为了兼容Android 4.1及以上版本,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()方法来构建和设置配置。但是,不是所有配置选项都可以使用用此方法。
从Android 4.0开始,可以在清单service元素中包含<meta-data>元素来引用一个xml无障碍配置文件,该配置文件可以设置无障碍服务的所有配置选项。代码示例:
<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"
/>
在运行时配置无障碍服务配置信息可以参考AccessibilityServiceInfo 类。
无障碍服务方法
无障碍服务必须继承AccessibilityService类并重写它的方法。这些方法按照Android系统调用的顺序,从服务启动调用(onServiceConnected()),运行时调用(onAccessibilityEvent(),onInterupt())到关闭服务调用(onUnbind())。
- onServiceConnected():可选方法。该方法在系统成功连接到无障碍服务时调用。可以在该方法中为无障碍服务做一次性设置操作,包括连接到用户反馈系统服务,比如音频管理器或设备振动器。该方法还可以在运行时设置无障碍服务配置或一次性调整操作,调用setServiceInfo()方法进行设置。
- onAccessibilityEvent():必选方法。当系统检测到与无障碍服务配置中指定事件帅选参数相匹配的AccessibilityEvent时会回调该方法。比如,当用户单击某个按钮或某个用户界面控件获得焦点时,系统会回调该方法,并传递关联的AccessibilityEvent,然后无障碍服务可以解释并向用户提供反馈。此方法可以在服务的生命周期中多次调用。
- onInterupt():必选方法。当系统想要中断服务提供的反馈时调用此方法,通常是响应用户操作。此方法可以在服务的生命周期中多次调用。
- onUnbind():可选方法。当系统即将关闭无障碍服务时调用此方法。使用此方法可执行任何一次性关闭程序,包括取消分配用户反馈系统服务,比如音频管理器或设备振动器。
注册无障碍事件(Event)
无障碍服务功能配置参数最重要的功能之一是允许指定服务可以处理某一类型的无障碍事件。能够指定处理某类型无障碍事件可以使无障碍功能相互协作,并使开发人员能够灵活地处理特定事件类型。事件过滤包含以下标准:
- 包名(package name):指定无障碍服务处理哪个应用程序的无障碍事件。如果缺省此参数,则默认无障碍服务可以处理所有应用程序的事件。此参数可以在无障碍服务配置文件中设置,使用android:packageNames属性且以逗号(,)分隔列表,或者使用AccessibilityServiceInfo.packageNames成员变量进行设置。
- 事件类型(Event Types):指定无障碍服务想要处理的事件类型。该参数可以在无障碍服务配置文件中设置,使用android:accessibilityEventTypes属性且以竖线(|)分隔列表(比如,accessibilityEventTypes="typeViewClicked|typeViewFocused"),或者使用AccessibilityServiceInfo.eventTypes成员变量进行设置。
设置无障碍服务时,请仔细考虑服务能够处理哪些事件,并只注册这些事件。由于用户一次可以激活多个无障碍服务,因此自己无障碍服务不得使用无法处理的事件。
示例代码
清单配置代码:
<service android:name=".accessibility.TaskBackService"
android:label="@string/accessibility_query_window_label"
android:enabled="@bool/atLeastIceCreamSandwich"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/taskbackconfig" />
</service>
xml配置代码:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:packageNames="com.example.android.apis"
android:accessibilityFeedbackType="feedbackSpoken"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_query_window_description" />
无障碍服务代码:
package com.example.android.apis.accessibility;
import com.example.android.apis.R;
import android.accessibilityservice.AccessibilityService;
import android.text.TextUtils;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityRecord;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import java.util.Locale;
/**
* This class demonstrates how an accessibility service can query
* window content to improve the feedback given to the user.
*/
public class TaskBackService extends AccessibilityService implements OnInitListener {
/** Tag for logging. */
private static final String LOG_TAG = "TaskBackService/onAccessibilityEvent";
/** Comma separator. */
private static final String SEPARATOR = ", ";
/** The class name of TaskListView - for simplicity we speak only its items. */
private static final String TASK_LIST_VIEW_CLASS_NAME =
"com.example.android.apis.accessibility.TaskListView";
/** Flag whether Text-To-Speech is initialized. */
private boolean mTextToSpeechInitialized;
/** Handle to the Text-To-Speech engine. */
private TextToSpeech mTts;
/**
* {@inheritDoc}
*/
@Override
public void onServiceConnected() {
// Initializes the Text-To-Speech engine as soon as the service is connected.
mTts = new TextToSpeech(getApplicationContext(), this);
}
/**
* Processes an AccessibilityEvent, by traversing the View's tree and
* putting together a message to speak to the user.
*/
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (!mTextToSpeechInitialized) {
Log.e(LOG_TAG, "Text-To-Speech engine not ready. Bailing out.");
return;
}
// This AccessibilityNodeInfo represents the view that fired the
// AccessibilityEvent. The following code will use it to traverse the
// view hierarchy, using this node as a starting point.
//
// NOTE: Every method that returns an AccessibilityNodeInfo may return null,
// because the explored window is in another process and the
// corresponding View might be gone by the time your request reaches the
// view hierarchy.
AccessibilityNodeInfo source = event.getSource();
if (source == null) {
return;
}
// Grab the parent of the view that fired 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 or not it's complete, based on
// the text inside the label, and the state of the check-box.
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.task_complete);
} else {
completeStr = getString(R.string.task_not_complete);
}
String taskStr = getString(R.string.task_complete_template, taskLabel, completeStr);
StringBuilder utterance = new StringBuilder(taskStr);
// The custom ListView added extra context to the event by adding an
// AccessibilityRecord to it. Extract that from the event and read it.
final int records = event.getRecordCount();
for (int i = 0; i < records; i++) {
AccessibilityRecord record = event.getRecord(i);
CharSequence contentDescription = record.getContentDescription();
if (!TextUtils.isEmpty(contentDescription )) {
utterance.append(SEPARATOR);
utterance.append(contentDescription);
}
}
// Announce the utterance.
mTts.speak(utterance.toString(), TextToSpeech.QUEUE_FLUSH, null);
Log.d(LOG_TAG, utterance.toString());
}
private AccessibilityNodeInfo getListItemNodeInfo(AccessibilityNodeInfo source) {
AccessibilityNodeInfo current = source;
while (true) {
AccessibilityNodeInfo parent = current.getParent();
if (parent == null) {
return null;
}
if (TASK_LIST_VIEW_CLASS_NAME.equals(parent.getClassName())) {
return current;
}
// NOTE: Recycle the infos.
AccessibilityNodeInfo oldCurrent = current;
current = parent;
oldCurrent.recycle();
}
}
/**
* {@inheritDoc}
*/
@Override
public void onInterrupt() {
/* do nothing */
}
/**
* {@inheritDoc}
*/
@Override
public void onInit(int status) {
// Set a flag so that the TaskBackService knows that the Text-To-Speech
// engine has been initialized, and can now handle speaking requests.
if (status == TextToSpeech.SUCCESS) {
mTts.setLanguage(Locale.US);
mTextToSpeechInitialized = true;
}
}
/**
* {@inheritDoc}
*/
@Override
public void onDestroy() {
super.onDestroy();
if (mTextToSpeechInitialized) {
mTts.shutdown();
}
}
}