@ZeroGeek
2018-05-16T00:03:12.000000Z
字数 6709
阅读 2234
根据统计,目前我国有1700多万视障人士,意味着平均每81人中就有一位视障人士可能会在使用互联网服务时遇到困难。目前随手记拥有3亿注册用户,为了让财务金融服务惠及每一位用户,为帮助视障人士轻松地进行记账、投资和学习财商知识,让他们能平等、方便、无障碍地获取信息和利用信息,我们对随手记Android进行了无障碍改造和优化。
Android产品的无障碍主要是针对视觉障碍人士,在设备的辅助功能中开启无障碍服务(如TalkBack)后,它能够读取屏幕上的文本信息,转化为语音提示,达到信息无障碍。
当出现绿区域并伴有语音提示的时候表示进入了无障碍模式。View能被正常选中,并有语音提示其文本信息,说明该View具有无障碍功能。
找到界面中所有有效的元素,设置文本信息。
简单代码示例:
// XML
<ImageButton
...
android:contentDescription="@string/share" />
// 代码
private void updateImageButton() {
if (mediaCurrentlyPlaying) {
playPauseImageView.setContentDescription(getString(R.string.pause));
} else {
playPauseImageView.setContentDescription(getString(R.string.play));
}
}
如上,有些界面的选中状态是通过设置ImageView的背景图片来控制的。无障碍服务无法识别,语音提示中不包含选中状态。
处理方法:
一、使用可以朗读选中状态的系统标准控件,如CheckBox或CheckedTextView。
二、给控件添加无障碍代理(AccessibilityDelegate),在onInitializeAccessibilityNodeInfo()方法中调用AccessibilityNodeInfo对象的setChecked方法设置选中状态。
我们使用的是第二种方式。具体实现如下:
rootView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setCheckable(true);
info.setChecked(itemData.isSelected());
}
});
图中第三方登录的微信图标和文本分别具有焦点,需要整合到一起。避免多余的操作,加快浏览。对于类似手机快捷注册文本按钮,应该扩大可触碰范围。
有些界面包含装饰性的元素,需要去除掉焦点。
例如:随手记更多界面的间隔块。
移除焦点代码示例:
android:focusable="false"
android:focusableInTouchMode="false"
android:importantForAccessibility="no"
改造过程:
1.先设置滚轮面板的焦点,保证可选中。
2.在滚轮Item选中的回调函数中,设置view的contentDescription属性同时发送无障碍事件。
// 防止频率过高,做了延时处理
private void sendAccessibilityViewSelectedEvent() {
postDelayed(new Runnable() {
@Override
public void run() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
}
}, 200L);
}
3.重载onPopulateAccessibilityEvent方法,添加描述文本
@Override
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
super.onPopulateAccessibilityEvent(event);
int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED
|| eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
if (viewAdapter != null) {
event.getText().add(viewAdapter.getItemContentDes(currentItem));
event.setItemCount(viewAdapter.getItemsCount());
event.setCurrentItemIndex(currentItem);
}
}
}
没处理之前就是一个块区,滑动没反应。
较好实现无障碍的方式是借助ExploreByTouchHelper。(笔者主要是参考了Android 5.1系统源码中LockPatternView类的无障碍实现)
下面给出了部分代码实现:
1.编写相应的ExploreByTouchHelper类,重载6个方法
private final class PatternExploreByTouchHelper extends ExploreByTouchHelper {
private Rect mTempRect = new Rect();
private HashMap<Integer, VirtualViewContainer> mItems = new HashMap<>();
private static final int VIRTUAL_BASE_VIEW_ID = 1;
/**
* 手势面板有9个点,每个点都做为一个虚拟节点,要根据x,y坐标获取对应的虚拟节点的编号(这个int值由自己约定)
* @return 其它返回ExploreByTouchHelper.INVALID_ID
*/
@Override
protected int getVirtualViewAt(float x, float y) {
final int rowHit = getRowHit(y);
if (rowHit < 0) {
return ExploreByTouchHelper.INVALID_ID;
}
final int columnHit = getColumnHit(x);
if (columnHit < 0) {
return ExploreByTouchHelper.INVALID_ID;
}
boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit];
int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID;
int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
return view;
}
/**
* 方法名有点奇怪,它的作用是把虚拟节点的编号放进List中
* 这里我们加了9个编号进来,1到9
* @param virtualViewIds
*/
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
if (!mPatternInProgress) {
return;
}
for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
if (!mItems.containsKey(i)) {
VirtualViewContainer item = new VirtualViewContainer(getTextForVirtualView(i));
mItems.put(i, item);
}
virtualViewIds.add(i);
}
}
/**
* 给每个虚拟节点填充事件,即手势面板中的9个点设置描述文本
*/
@Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
if (mItems.containsKey(virtualViewId)) {
CharSequence contentDescription = mItems.get(virtualViewId).description;
event.getText().add(contentDescription);
}
}
/**
* 给宿主View填充事件,即手势面板设置描述文本
*/
@Override
public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
super.onPopulateAccessibilityEvent(host, event);
if (!mPatternInProgress) {
CharSequence contentDescription = getContext().getText(R.string.lock_pattern_area);
event.setContentDescription(contentDescription);
}
}
/**
* 给虚拟View设置描述文本和边框
* 边框是指无障碍模式下选中的区块边界
* @param virtualViewId
* @param node
*/
@Override
protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node) {
node.setText(getTextForVirtualView(virtualViewId));
node.setContentDescription(getTextForVirtualView(virtualViewId));
if (mPatternInProgress) {
node.setFocusable(true);
if (isClickable(virtualViewId)) {
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
node.setClickable(isClickable(virtualViewId));
}
}
final Rect bounds = getBoundsForVirtualView(virtualViewId);
node.setBoundsInParent(bounds);
}
/**
* 提供交互,触发回调重绘控件
*/
@Override
protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) {
switch (action) {
case AccessibilityNodeInfo.ACTION_CLICK:
return onItemClicked(virtualViewId);
default:
break;
}
return false;
}
// ...
}
2.在构造函数中设置无障碍代理
public LockPatternView(Context context) {
// something else
// ...
// 无障碍代理
mPatternTouchHelper = new PatternExploreByTouchHelper(this);
ViewCompat.setAccessibilityDelegate(this, mPatternTouchHelper);
}
3.在LockPatternView中实现onHoverEvent()和dispatchHoverEvent()
@Override
public boolean onHoverEvent(MotionEvent event) {
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_HOVER_ENTER:
event.setAction(MotionEvent.ACTION_DOWN);
break;
case MotionEvent.ACTION_HOVER_MOVE:
event.setAction(MotionEvent.ACTION_MOVE);
break;
case MotionEvent.ACTION_HOVER_EXIT:
event.setAction(MotionEvent.ACTION_UP);
break;
case MotionEvent.ACTION_CANCEL:
event.setAction(MotionEvent.ACTION_CANCEL);
}
onTouchEvent(event);
event.setAction(action);
return super.onHoverEvent(event);
}
@Override
protected boolean dispatchHoverEvent(MotionEvent event) {
boolean handled = super.dispatchHoverEvent(event);
handled |= mPatternTouchHelper.dispatchHoverEvent(event);
return handled;
}
4.手势状态(如完成、中断等)的回调函数中要调用announceForAccessibility()提示用户。
在实现无障碍的同时,也解决了自定义View的UI自动化测试问题。无障碍需要不断更新迭代、优化。对此团队也制定了无障碍编码规范,列入代码审查要点中,来保证产品持续提供良好的无障碍功能。