在安卓开发过程中,为了视觉和功能的需要开发者经常会使用自定义视图
大多数的自定义视图是组合现有的控件来完成特定的功能
但是,有一种自定义视图是通过画笔在画布上画出自定义的子视图的,例如日期控件,颜色选择面板等
由于自定义视图的子视图是用画笔绘制的,所以这些子视图无法被无障碍服务访问
为了解决此种问题,Android系统在API16引入虚拟视图概念
开发人员可以通过虚拟视图模拟出视图结构,从而让无障碍服务能够访问这些绘制的子视图
今天就来讲讲使用支持库中的ExploreByTouchHelper工具类实现虚拟视图的方法:
1. 为自定义视图添加无障碍代理
在自定义视图初始化时,调用setAccessibilityDelegate()方法设置无障碍代理
参数是实现了ExploreByTouchHelper工具类的对象
如果想支持API更早的版本可以调用ViewCompat的setAccessibilityDelegate()方法,如下所示:
mTouchHelper = new CustomViewTouchHelper(this);ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
2. 实现无障碍代理
通过继承ExploreByTouchHelper工具类实现无障碍代理,如下所示:
private class CustomViewTouchHelper extends ExploreByTouchHelper {
public CustomViewTouchHelper(VirtualSubview view) {
super(view);
}...}
3. 为虚拟视图添加子视图节点
通过重写getVisibleVirtualViews()方法确定虚拟视图中有多少子节点
添加的子节点就是无障碍服务访问时能访问到的无障碍焦点
代码样例如下:
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
final List<VirtualView> childs = mChildren;
final int count = childs.size();
for (int i = 0; i < count; i++) {
VirtualView child = childs.get(i);
virtualViewIds.add(child.mId);
}}
4. 为虚拟视图节点添加ID
当用户开启无障碍服务访问虚拟节点时,会调用getVirtualViewAt()方法确定用户触摸的区域属于哪一个子视图
我们需要在此方法中通过x和y坐标计算出当前操作的虚拟节点的id
代码如下所示:
@Override
protected int getVirtualViewAt(float x, float y) {
VirtualView view = findVirtualViewByBoords(x, y);
if (view == null)
return INVALID_ID; //返回无效的节点return view.mId;
}
5. 为虚拟视图填充必要的无障碍属性
为了让无障碍服务正确地反馈虚拟视图的相关信息,我们需要为虚拟视图填充必要的无障碍属性信息
下面是填充信息的方法:
//在此方法中设置虚拟视图的无障碍事件信息
@Override
protected void onPopulateEventForVirtualView( int virtualViewId,AccessibilityEvent event) {
//调用此方法给无障碍事件填充text字段,text字段会被TalkBack朗读出来VirtualView item = findVirtualViewById(virtualViewId);
if (item != null)event.getText().add(item.mText);
}//调用此方法填充子虚拟视图的无障碍nodeinfo的属性
@Override
protected void onPopulateNodeForVirtualView(int virtualViewId,AccessibilityNodeInfoCompat node) {
//调用此方法在NodeInfo中设置子虚拟视图的text字段,此字段会被talkback朗读出来VirtualView item = findVirtualViewById(virtualViewId);
if (item == null)
return;node.setText(item.mText);
Rect bounds = item.mBounds;
//调用此方法设置子虚拟视图的焦点大小,焦点大小与实际画的视图一致大小。此方法必须调用。node.setBoundsInParent(bounds);//调用此方法设置nodeinfo都能处理哪些无障碍事件。调用此方法只能说明nodeinfo能处理这些action,不是触发action,也不是具体处理action。这里可以设置多个action。node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
//调用此方法代表此nodeinfo节点是可以被选中。当设置为true代表可以被选中,如复选框就需要设置为true。node.setCheckable(true);
//设置无障碍属性的选中状态
node.setChecked(item.mAlpha == VirtualView.ALPHA_SELECTED);
}
6. 响应无障碍事件
添加了虚拟视图的自定义控件要响应无障碍服务的相关事件,如点击
需要做下面两个步骤:
第一步:
重写dispatchHoverEvent()方法,并且把事件转发给实现了ExploreByTouchHelper的对象处理,如下所示:
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)@Overridepublic boolean dispatchHoverEvent(MotionEvent event) {
if (mTouchHelper.dispatchHoverEvent(event)) {
return true;
}
第二步:
在辅助类中重写onPerformActionForVirtualView()方法,在此方法中处理相关的无障碍事件
事件处理完成返回true,未处理返回false,从而让系统自动处理
下面的代码中处理了点击事件:
@Override
protected boolean onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments) {
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_CLICK:
//处理点击事件VirtualView view = findVirtualViewById(virtualViewId);if (view == null)return false;setVirtualViewSelected(view, !(view.mAlpha == VirtualView.ALPHA_SELECTED) );invalidate();
mTouchHelper.invalidateVirtualView(virtualViewId);return true;
}return false;
}
注意:当虚拟节点中的无障碍属性更改后,需要调用invalidateVirtualView()更新指定的无障碍节点
如果不更新无障碍服务获取的信息会出现问题,如焦点显示错误、文本提示错误、状态朗读错误等
以上就是自定义视图实现虚拟节点的方法
借助ExploreByTouchHelper工具类实现虚拟节点我们只需要重写对应的方法、添加无障碍代理、转发事件就能轻松的完成
比使用AccessibilityNodeProvider类简单很多
下面贴出完整的选择颜色的自定义视图代码以供参考:
public class VirtualSubview extends View {private final Paint mPaint = new Paint();
private final Rect mTempRect = new Rect();
private final List<VirtualView> mChildren = new ArrayList<VirtualView>();
private VirtualSubview mLastHoveredChild;
private CustomViewTouchHelper mTouchHelper;
private Context context;public VirtualSubview(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
createVirtualChildren();
mTouchHelper = new CustomViewTouchHelper(this);
ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
}@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public boolean dispatchHoverEvent(MotionEvent event) {
if (mTouchHelper.dispatchHoverEvent(event)) {
return true;
}return super.dispatchHoverEvent(event);
}@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int offsetX = 0;
List<VirtualView> children = mChildren;
final int childCount = children.size();
for (int i = 0; i < childCount; i++) {
VirtualView child = children.get(i);
Rect childBounds = child.mBounds;
childBounds.set(offsetX, 0, offsetX + childBounds.width(), childBounds.height());
offsetX += childBounds.width();
}
}@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 0;
int height = 0;
List<VirtualView> children = mChildren;
final int childCount = children.size();
for (int i = 0; i < childCount; i++) {
VirtualView child = children.get(i);
width += child.mBounds.width();
height = Math.max(height, child.mBounds.height());
}
setMeasuredDimension(width, height);
}@Override
protected void onDraw(Canvas canvas) {
Rect drawingRect = mTempRect;
List<VirtualView> children = mChildren;
final int childCount = children.size();
for (int i = 0; i < childCount; i++) {
VirtualView child = children.get(i);
drawingRect.set(child.mBounds);
mPaint.setColor(child.mColor);
mPaint.setAlpha(child.mAlpha);
canvas.drawRect(drawingRect, mPaint);
}
}@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
VirtualView view = findVirtualViewByBoords(event.getX(), event.getY());
if (view == null)
return true;setVirtualViewSelected(view, !(view.mAlpha == VirtualView.ALPHA_SELECTED) );
sendAccessibilityEventForVirtualView(view, AccessibilityEvent.TYPE_VIEW_CLICKED);
invalidate();
mTouchHelper.invalidateVirtualView(view.mId);
return true;
}
return super.onTouchEvent(event);
}private void createVirtualChildren() {
VirtualView firstChild = new VirtualView(0, new Rect(0, 0, 150, 150), Color.RED,
"Virtual view 1");
mChildren.add(firstChild);
VirtualView secondChild = new VirtualView(1, new Rect(0, 0, 150, 150), Color.GREEN,
"Virtual view 2");
mChildren.add(secondChild);
VirtualView thirdChild = new VirtualView(2, new Rect(0, 0, 150, 150), Color.BLUE,
"Virtual view 3");
mChildren.add(thirdChild);
}private void setVirtualViewSelected(VirtualView virtualView, boolean selected) {
virtualView.mAlpha = selected ? VirtualView.ALPHA_SELECTED : VirtualView.ALPHA_NOT_SELECTED;
}private void sendAccessibilityEventForVirtualView(VirtualView virtualView, int eventType) {
if (mAccessibilityManager.isTouchExplorationEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
event.setPackageName(getContext().getPackageName());
event.setClassName(virtualView.getClass().getName());
event.setSource(VirtualSubview.this, virtualView.mId);
event.getText().add(virtualView.mText);
getParent().requestSendAccessibilityEvent(VirtualSubview.this, event);
}
}private VirtualView findVirtualViewById(int id) {
List<VirtualView> children = mChildren;
final int childCount = children.size();
for (int i = 0; i < childCount; i++) {
VirtualView child = children.get(i);
if (child.mId == id) {
return child;
}
}
return null;
}private class VirtualView {
public static final int ALPHA_SELECTED = 255;
public static final int ALPHA_NOT_SELECTED = 127;
public final int mId;
public final int mColor;
public final Rect mBounds;
public final String mText;
public int mAlpha;public VirtualView(int id, Rect bounds, int color, String text) {
mId = id;
mColor = color;
mBounds = bounds;
mText = text;
mAlpha = ALPHA_NOT_SELECTED;
}
}private class CustomViewTouchHelper extends ExploreByTouchHelper {
public CustomViewTouchHelper(
VirtualSubview view) {
super(view);
}//通过此方法的x和y参数来确定旭虚拟视图的哪一个虚拟子视图的虚拟id。在此方法中调用自己实现的通过x、y坐标获取id的方法,id通常是0、1、2……
@Override
protected int getVirtualViewAt(float x, float y) {
VirtualViewview = findVirtualViewByBoords(x, y);
if (view == null)
return INVALID_ID;return view.mId;
}//调用此方法来确定虚拟视图中的哪些子视图有无障碍焦点。加入列表中的虚拟id的子虚拟视图都有无障碍焦点
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
final List<VirtualView> childs = mChildren;
final int count = childs.size();
for (int i = 0; i < count; i++) {
VirtualView child = childs.get(i);
virtualViewIds.add(child.mId);
}}//在此方法中填充子虚拟视图的无障碍事件中的属性
@Override
protected void onPopulateEventForVirtualView(
int virtualViewId, AccessibilityEvent event) {
//调用此方法给无障碍事件填充text字段,text字段会被TalkBack朗读出来
VirtualView item = findVirtualViewById(virtualViewId);
if (item != null)
event.getText().add(item.mText);
}//调用此方法填充子虚拟视图的无障碍nodeinfo的属性
@Override
protected void onPopulateNodeForVirtualView(
int virtualViewId, AccessibilityNodeInfoCompat node) {
//调用此方法在NodeInfo中设置子虚拟视图的text字段,此字段会被talkback朗读出来
VirtualView item = findVirtualViewById(virtualViewId);
if (item == null)
return;node.setText(item.mText);
Rect bounds = item.mBounds;
//调用此方法设置子虚拟视图的焦点大小,焦点大小与实际画的视图一致大小。此方法必须调用。
node.setBoundsInParent(bounds);//调用此方法设置nodeinfo都能处理哪些无障碍事件。调用此方法只能说明nodeinfo能处理这些action,不是触发action,也不是具体处理action。这里可以设置多个action。
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);//调用此方法代表此nodeinfo节点是可以被选中。当设置为true代表可以被选中,如复选框就需要设置为true。
node.setCheckable(true);
node.setChecked(item.mAlpha == VirtualView.ALPHA_SELECTED);
}//调用此方法具体处理无障碍事件的action。在此方法中需要根据不同的action进行处理,当发生click后调用普通的点击处理方法,在此案例中调用的是onitemclick()方法。
@Override
protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) {
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_CLICK:
VirtualView view = findVirtualViewById(virtualViewId);
if (view == null)
return false;setVirtualViewSelected(view, !(view.mAlpha == VirtualView.ALPHA_SELECTED) );
invalidate();
mTouchHelper.invalidateVirtualView(virtualViewId);
return true;}return false;
}}public VirtualView findVirtualViewByBoords(float x, float y) {
final List<VirtualView> childs = mChildren;
final int count = childs.size();
for (int i = 0; i < count; i++) {
VirtualView child = childs.get(i);
if (child.mBounds.contains( (int)x, (int)y))
return child;
}
return null;
}
}
更多精彩干货:欢迎关注“无障碍实验室”公众号