Android无障碍总结

说起Android无障碍,也许很多同学没听说过,那这里我就来扫盲一下。许多Android用户有不同的能力(限制),这要求他们以不同的方式使用他们的Android设备。这些限制包括视力,肢体或与年龄有关,这些限制阻碍了他们看到或充分使用触摸屏,而用户的听力丧失,让他们可能无法感知声音信息和警报。Android提供了辅助功能的特性和服务帮助这些用户更容易的使用他们的设备,这些功能包括语音合成、触觉反馈、手势导航、轨迹球和方向键导航。Android应用程序开发人员可以利用这些服务,使他们的应用程序更贴近用户。

当然,我们这里无障碍化的实现主要是针对盲人用户,以语音的方式提示操作。许多用户界面控件依赖视觉线索来表示他们的意义和用法。例如,一个记笔记的应用程序可能会使用一个带加号图片的ImageButton表示用户可以添加一条新的笔记。在一个EditText组件旁边可能会有一个标签来说明需要输入的内容。视力较弱的用户看不到我们给出这些提示,这使得这些提示毫无用处。你可以使用在XML布局中的android:contentDescription 属性使这些控件更容易理解。 添加了这个属性的文本并不出现在屏幕上,但如果用户打开了提供声音提示的辅助功能服务,那么当用户进行访问控制时,文本会被讲出来。出于这个原因,将android:contentDescription属性应用在你应用程序用户界面的每个ImageButton ImageView,CheckBox上,并且在其他输入控件中添加该属性,对于无法看到输入控件的用户,这些额外的信息是很有必要的。

要想测试下无障碍的体验,我们需要以下几步:1.下载TalkBack辅助工具。2.下载讯飞语音软件。3到设置里面打开高级工具--->辅助功能--->打开TalkBack。完成上面这三步你就可以听到系统读出来的操作提示语音了。此时你是不是觉得有点麻烦,咋这么多步骤呢,盲人操作起来不是更费力吗?其实偷偷跟你说,盲人的手机里这些设置是一直打开的,所以你不用担心盲人使用这些功能会不方便。

上面废话了这么多,下面就直接进主题了。那我们的代码中要怎么实现呢?有两条线索:第一,如果你使用的控件是系统提供的,那么我们只需要在布局文件里面给每个控件添加android:contentDescription 属性,可以类似下面这样:

<ImageButton
                android:id="@+id/icon"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginLeft="8dp"
                android:layout_marginRight="8dp"
                android:background="@drawable/trans"
                android:contentDescription="添加图片按钮"
                android:src="@drawable/photo_pack" />

 很简单,只要这样设置就可以了。由于添加了android:contentDescription 这个属性,当用户移动焦点到这个按钮或将鼠标悬停在它上面时,提供口头反馈的辅助功能服务就会发出“添加图片按钮”提示用户进行添加图片操作。注意:对于EditText控件,提供了一个android:hint属性代替了contentDescription属性,这个属性可以在内容为空时,提示用户应该输入的内容。当输入完成后,TalkBack 读出输入内容给用户,不再是提示的文本内容。对于系统控件还有一种方式可以实现无障碍,类似于下面这种:

ImageView rightMoreButton = getRightMoreButton();
if (rightMoreButton != null) {
	 getRightMoreButton().setContentDescription("更多");
}

 对于系统控件,总结起来就是要么在布局文件中设置android:contentDescription 属性,要么就在动态代码里面setContentDescription()。第二,如果我们使用的是自定义控件或者自绘控件,采用上面的方法实现无障碍是行不通的,但方法还是有的,那就是今天的重头戏:虚拟节点。

对于采用虚拟节点实现无障碍,我们需要在自定义控件的内部写一个类继承自ExporedByTouchHelper这个类,并且实现其中的5个关键方法。

//首先声明一个无障碍辅助类的变量
protected XXXTouchHelper touchHelper;
//然后初始化无障碍辅助类,初始化的时机可以在构造方法中,也可以在onMeasure方法中
 if (isAccessibilityEnable()) {//当开启无障碍设置时才执行
      if(touchHelper==null) {
          touchHelper = new XXXTouchHelper(this);
          //无障碍委托
          ViewCompat.setAccessibilityDelegate(this, touchHelper);
          //开启无障碍
          ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
      }
 }
/**
**开始实现这个无障碍辅助类
**/
protected class XXXTouchHelper extends ExploreByTouchHelper {
       HashMap<Integer, SubAreaShell> areaShells = new HashMap<Integer, SubAreaShell>(2);//存放找到的虚拟控件

        public XXXTouchHelper(View host) {
            super(host);
        }

        /**
         * 如果自绘view中含有多个控件,每个控件相当于一个虚拟节点,这里根据x,y坐标获取对应的虚拟节点的编号
         * 这里的编号是你自己定的,比如说我这个XXXView里面有两个自定义控件XXXTextArea和XXXSinglePicArea
         * 且XXXTextArea出现在XXXPicArea的上方,那么XXXTextArea的序号就为0,XXXSinglePicArea的序号就为1
         * @param x
         * @param y
         * @return 找不到虚拟节点就返回ExploreByTouchHelper.INVALID_ID
         */
        @Override
        protected int getVirtualViewAt(float x, float y) {

            //这里写你自己的代码逻辑,判断x,y坐标被包含在那个虚拟节点里面,我这里简单写个示例代码
            SubAreaShell area = findArea(x, y);
            if (area != null) {
                if(area.getSubArea() instanceof XXXTextArea){
                    return 0;
                }else if(area.getSubArea() instanceof XXXSinglePicArea){
                    return 1;
                }

            }
            return ExploreByTouchHelper.INVALID_ID;
        }

        /**
         * 这个方法的命名使人误会,其实它主要的作用就是把你上面找到的虚拟节点的编号放进List中
         * @param virtualViewIds
         */
        @Override
        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {

            //如果实际情况比较复杂的话,这里可以灵活变化下
            if (areaShells.size() > 2){ //areaShells只存储两个元素
                areaShells.clear();
            }
            for (SubAreaShell subAreaShell : mAreasList) {//mAreasList是外面类的一个变量,负责把各个view收集起来
                SubArea subArea = subAreaShell.getSubArea();

                if (subArea instanceof XXXTextArea) {
                    if (subArea.getType() == ViewArea.TYPE_NORMAL_SUMMARY) {
                        virtualViewIds.add(0);//主要
                        areaShells.put(0, subAreaShell);
                    }
                } else if (subArea instanceof XXXSinglePicArea) {
                    virtualViewIds.add(1);//主要
                    areaShells.put(1, subAreaShell);
                }
            }
        }

        /**
         * 给虚拟view填充事件,即是给对应的虚拟控件设置文字描述
         * @param virtualViewId
         * @param event
         */
        @Override
        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
            if(virtualViewId == 0){
                event.setContentDescription("我是文字");
            }else{
                event.setContentDescription("我是图片");
            }
        }

        /**
         * 给虚拟view填充节点,即是给虚拟节点设置文字描述和无障碍边框
         * @param virtualViewId
         * @param node
         */
        @Override
        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node) {
            //node.setContentDescription,node.setBoundsInParent必须要设置,不然会报异常,后面我会说
            if(virtualViewId == 0){
                node.setContentDescription("我是文字");
            }else{
                node.setContentDescription("我是图片");
            }
            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);//给虚拟节点添加无障碍点击动作

            Rect bound = getBoundsForIndex(virtualViewId);//根据上面放进来的虚拟id寻找对应区域的无障碍边框
            if (bound.isEmpty()) {
                bound = new Rect(0, 0, 1, 1);    //很无奈的一种保护,防止下一步crash
            }
            node.setBoundsInParent(bound);//必须保证bound不是empty,不然会报错
        }

        /**
         * 计算每一个区域的无障碍边框
         * @param virtualViewId
         * @return
         */
        public Rect getBoundsForIndex(int virtualViewId) {
            Rect rect = new Rect();
            SubAreaShell subAreaShell;
            SubArea subArea;

            //这是我的逻辑代码,仅供参考
            if (virtualViewId != ExploreByTouchHelper.INVALID_ID) {
                if (virtualViewId == 0) {
                    subAreaShell = areaShells.get(0);
                    if (subAreaShell == null){
                        return rect;    //返回一个空Rect
                    }
                    subArea = subAreaShell.getSubArea();
                    if (subArea instanceof XXXTextArea) {
                        XXXTextArea textArea = (XXXTextArea) subArea;
                        rect.left = 0;
                        rect.top = 0;
                        rect.right = XXXView.this.getWidth();
                        rect.bottom = subAreaShell.getTop() + textArea.getHeight();
                    }

                } else {
                    subAreaShell = areaShells.get(1);
                    if (subAreaShell == null){
                        return rect;    //返回一个空Rect
                    }
                    subArea = subAreaShell.getSubArea();
                    if (subArea instanceof XXXSinglePicArea) {
                        XXXSinglePicArea singlePicArea = (XXXSinglePicArea) subArea;
                        rect.left = singlePicArea.getPaddingLeft();
                        rect.top = singlePicArea.getMarginTop();
                        rect.right = rect.left + singlePicArea.getWidth();
                        rect.bottom = rect.top + singlePicArea.getHeight();
                    }
                }
            }
            return rect;
        }

        /**
         * 提供无障碍交互
         * @param virtualViewId
         * @param action
         * @param arguments
         * @return
         */
        @Override
        protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) {
            switch(action){
                case AccessibilityNodeInfoCompat.ACTION_CLICK:
                    onFeedContentViewClick(virtualViewId);
                    return true;
            }
            return false;
        }

    }

    protected void onFeedContentViewClick(int virtualViewId) {
        super.playSoundEffect(SoundEffectConstants.CLICK);
        if(touchHelper==null)
            return;
        touchHelper.invalidateVirtualView(virtualViewId);
        touchHelper.sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED);
    }

    /**
     * 分发触摸事件
     * @param event
     * @return
     */
    @Override
    protected boolean dispatchHoverEvent(MotionEvent event) {
        if(FeedEnv.isAccessibilityEnable() &&
                touchHelper!=null && touchHelper.dispatchHoverEvent(event)){
            return true;
        }
        return super.dispatchHoverEvent(event);
    }

 好了,到了这里你已经可以用虚拟节点的方法实现无障碍了。棒耶!!

需要注意的点:

1.
要实现ExploreByTouchHelper里面的这几个方法,我例子里面都有注释啦
getVirtualViewAt
getVisibleVirtualViews
onPopulateEventForVirtualView
onPopulateNodeForVirtualView
onPerformActionForVirtualView
外加一个
dispatchHoverEvent
2.
请注意以以下的关键点,否则要跪,虚拟节点实现无障碍本来就是个变幻莫测的东西
a.在getVirtualViewAt()方法中获取对应虚拟节点的rect编号的时候,这个rect要和在populateNodeVirtualView中
  设置的setBoundsInParent()里面的rect(大小)一致,否则无障碍模式的点击操作不起作用!

b.一定要在onPopulateNodeForVirtualView中设置node.setContentDescription和node.setBoundsInParent,
  不然会抛异常,不信你看源码
if(event.getText().isEmpty() && event.getContentDescription() == null) {
            throw new RuntimeException("Callbacks must add text or a content description in populateEventForVirtualViewId()");
        }
node.getBoundsInParent(this.mTempParentRect);
if(this.mTempParentRect.isEmpty()) {
                throw new RuntimeException("Callbacks must set parent bounds in populateNodeForVirtualViewId()");
            }

c.node.setBoundsInParent(bound);必须保证bound不是empty,不然会挂,不信你看源码是怎么抛异常的
if(this.mTempParentRect.isEmpty()) {
                throw new RuntimeException("Callbacks must set parent bounds in populateNodeForVirtualViewId()");
            }

下面是活生生的例子啊,同志们
错误类型:java.lang.RuntimeException
Crash详情:
java.lang.RuntimeException: Callbacks must set parent bounds in populateNodeForVirtualViewId()
android.support.v4.widget.ExploreByTouchHelper.createNodeForChild(ProGuard:389)
android.support.v4.widget.ExploreByTouchHelper.createNode(ProGuard:318)
android.support.v4.widget.ExploreByTouchHelper.access$100(ProGuard:52)
android.support.v4.widget.ExploreByTouchHelper$ExploreByTouchNodeProvider.createAccessibilityNodeInfo(ProGuard:710)

 最后,做无障碍模块让我见识到一个很励志的事情:这个世界上居然真的有盲人开发!而且还很积极地讨论无障碍的编程知识以及代码实现,无时无刻不被他的求知欲感动。盲人做程序员不容易,致敬!!

猜你喜欢

转载自blog.csdn.net/u012571415/article/details/79274161