无障碍服务是一个应用程序,它给有残疾的用户或暂时无法与设备完全交互的用户提供了更好的无障碍用户交互功能。比如驾驶、照顾小孩或者在吵闹的派对上可能需要额外或者替代的交互反馈。
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权限,来确保只有系统可以绑定无障碍服务。代码示例:
无障碍服务还必须提供相关配置,来指定服务处理的无障碍功能事件的类型以及有关该服务的其他信息。无障碍服务的配置信息包含在AccessibilityServiceInfo 类中,无障碍服务可以在运行时使用该类实例和setServiceInfo()方法来构建和设置配置。但是,不是所有配置选项都可以使用用此方法。
从Android 4.0开始,可以在清单service元素中包含
...
在运行时配置无障碍服务配置信息可以参考AccessibilityServiceInfo 类。
无障碍服务必须继承AccessibilityService类并重写它的方法。这些方法按照Android系统调用的顺序,从服务启动调用(onServiceConnected()),运行时调用(onAccessibilityEvent(),onInterupt())到关闭服务调用(onUnbind())。
无障碍服务功能配置参数最重要的功能之一是允许指定服务可以处理某一类型的无障碍事件。能够指定处理某类型无障碍事件可以使无障碍功能相互协作,并使开发人员能够灵活地处理特定事件类型。事件过滤包含以下标准:
设置无障碍服务时,请仔细考虑服务能够处理哪些事件,并只注册这些事件。由于用户一次可以激活多个无障碍服务,因此自己无障碍服务不得使用无法处理的事件。
清单配置代码:
xml配置代码:
无障碍服务代码:
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();
}
}
}