无障碍开发原理与实践

一、无障碍开发原理与实践

android无障碍开发是帮忙视障人士也能便捷地使用APP。Android本身提供了标准的无障碍服务,本文主要是搜索模块在开发无障碍服务的一些实践总结。大部分手机系统已支持无障碍,首先用户需要开启无障碍,选择Android设备 “设置->无障碍”,打开TalkBack开关。

浏览方式,开启TalkBack后,可以通过触摸功能浏览屏幕,也可以线性方式滑动浏览屏幕上的各项内容。

比如RecyclerView控件自带手指

线性导航模式要按照一次查看一项内容的方式浏览屏幕,请用一根手指向左或向右滑动,以便按顺序浏览各项内容。当您聚焦于某个项目时,TalkBack 会短暂延迟,随后提示您可以针对该项目执行哪些操作。

二、android无障碍常见用法说明

       如果想要使用无障碍,首先需要开启无障碍服务,打开TalkBack服务

       在手机中设置。

Android有一部分控件本身支持无障碍,具备获取无障碍焦点,朗读控件角色和控件状态,比如:TextView、Button、ImageView、RecyclerView和CheckBox控件本身支持无障碍朗读。还有一些控件,比如自定义View,LinearLayout,RelativeLayout和FrameLayout等控件本身无法获取无障碍焦点,也不朗读控件角色和控件状态。

一般情况,可以通过添加描述性文字设置无障碍朗读文本。

1. 在布局xml中,给控件添加属性android:contentDescription;

2、通过代码调用setContentDescription方法;

如果一个控件本身不支持获取无障碍焦点,可以通过两种方式设置获取无障碍焦点。

1. 在布局xml中添加android:focusable="true"或android:focusableInTouchMode="true",经过本人验证,两个属性添加一个即可。

2. 调用View的setFocusable或setFocusableInTouchMode方法,入参传true。

三、实际开发场景

接下来,通过几种场景描述了无障碍的应用,有实例代码可以直接使用。

实践一:如果想屏蔽无障碍朗读:可以通过以下代码解决

以搜索模块为例:搜索结果页在打开时自动朗读全部内容,这个并不是最好的效果,可以给容器屏蔽无障碍,当容器部子View获取到焦点后朗读。

ViewCompat.setImportantForAccessibility(view,ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);

说明:view:入参,传入需要屏蔽无障碍的控件。
 
 在布局文件对应节点下添加属性:android:importantForAccessibility="no"
在java代码中添加以下代码:
 
分为两类控件:1)本身具备获取无障碍焦点和2)本身不具备获取无障碍焦点的控件。
本身具备获取无障碍焦点的控件有:TextView,Button等
本身不具备获取无障碍焦点的控件有:LinearLayout,RelativeLayout和View等。
所以,如果想要这些控件获取无障碍焦点,可以通过两种方式:
(1)在布局文件中添加属性的方法,比如android:focusable="true",或android:focusableInTouchMode="true"经过自测,这两个属性添加其中任何一个都可以。
(2)也可以通过java代码的方式调用相应控件的setFocusable(true)和setFocusableInTouchMode(true)方法。
 

实践二:TextView控件标签朗读支持text和contentDescription两种

对于TextView控件来说,无障碍会朗读布局文件这两个属性配置的文案内容,只需二选一设置即可,在xml中添加属性:android:text或android:contentDescription,除了可通过在xml中设置外,还可通过代码调用setText和setContentDescription方法添加无障碍支持。

实践三:本身自带控件角色和控件状态朗读,只需添加无障碍标签即可

​          RecyclerView是一个列表控件,自带朗读角色和状态,“**选项,在列表中”,CheckBox是一个复选框,本身自带角色和状态朗读,本身会朗读“ 已选中,某某 复选框 ”,只需要添加标签,可实现无障碍支持。

实践四:控件获取无障碍焦点

          在一些场景下,比如选中列表某一项,然后刷新整个列表。刚才选中的列表项失去无障碍焦点,所以在整个列表刷新后,需要手动添加获取无障碍焦点。以京东App搜索结果页的筛选面板为例,筛选面板中部分筛选项是通过列表实现的,比如品牌,选中某一个品牌时,刷新整个列表项更新选中态,列表刷新后无障碍焦点丢失,这时就需要调用无障碍获取焦点的方法。示例代码如下:

         调用视图对象的sendAccessibilityEvent方法,传入获取焦点事件类型(TYPE_VIEW_HOVER_ENTER

view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);

实践五:如何让普通视图选中时,自动朗读角色状态呢?

对于无法自动朗读视图状态的控件,让控件在无障碍模式下朗读角色状态的方法,可以通过给视图添加无障碍代理实现。无障碍代理对象调用setSelected方法设置选中状态,选中状态设置成视图的选中态的值。控件选中时,调用对象的setSelected方法传入true,不能将“已选中”文案写在标签中。

public static void handleAccessibilityOfViewSelected(View view, final String text){
        if(view == null){
            return;
        }
        ViewCompat.setAccessibilityDelegate(view, new AccessibilityDelegateCompat() {
            @Override
            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
                super.onInitializeAccessibilityNodeInfo(host, info);
                info.setSelected(host.isSelected());
                info.setContentDescription(text);
            }
        });
 }

实践六:普通视图选中和CheckBox一样,朗读"已选中,复选框”或者“未选中,复选框"

CheckBox控件,自带的无障碍朗读,选中状态朗读“已选中,** 复选框”,未选中朗读“未选中,**复选框”。

如果一个控件本身不具备无障碍朗读效果,可通过设置一个无障碍代理,包装成CheckBox,这样普通View具备了CheckBox控件的无障碍特性,可朗读控件角色和状态。实例代码如下:

public static void handleAccessibilityOfViewChecked(View view) {
        if(view == null){
            return;
        }
        ViewCompat.setAccessibilityDelegate(view, new AccessibilityDelegateCompat() {
            @Override
            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
                super.onInitializeAccessibilityNodeInfo(host, info);

`                info.setClassName(CheckBox.class.getName());
                info.setCheckable(true);
                info.setSelected(false);
                info.setChecked(host.isSelected());
            }

            @Override
            public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
                super.onInitializeAccessibilityEvent(host, event);

                if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) {
                    event.setChecked(host.isSelected());
                }
            }
        });
    }

实践七:朗读“某某控件已停用”

有一些业务场景,不支持用户选择操作。举个例子:比如当商品无货时,不支持用户选择商品,这时候可以将控件设置为不可用状态。方法也特别简单,调用View类的setEnabled方法,参数传false,无障碍朗读:“某某控件已停用”。

实践八:去除无障碍角色朗读

       带有某个角色的控件也会带有一些特性,比如RecyclerView控件,此操作要谨慎使用,因为移除控件的角色外,控件本身具有的属性也会消失。

视图的无障碍控件角色与视图设置的无障碍代理中AccessibilityNodeInfo对象设置的类名称有关。

有一些控件类,本身具备无障碍朗读特性,比如:CheckBox控件,设置了文本名称“Android”,未选中时朗读“ 未选中,Android,复选框”,已选中朗读 “已选中,Android 复选框 ”。如果想屏蔽角色朗读,可以通过给无障碍代理对象设置成不能朗读角色的控件,也就是调用AccessibilityNodeInfo对象的setClassName方法,传入空或其他不支持朗读角色的控件类名称。屏蔽之后朗读“未选中,Android”,选中之后朗读“已选中,Android”。

这个方法在使用上要谨慎,因为角色更改,除了不能朗读控件角色外,控件自带的一些属性也会消失。举个例子:比如一个横向RecyclerView列表,一屏展示不下,可通过无障碍扫动模式自动翻到下一页,实现扫动方式浏览全部列表项。去除无障碍角色的RecyclerView,无法通过扫动方式实现翻页朗读的效果。所以在使用上,需根据业务场景选择是否有必要屏蔽,并且需要多加测试。

public static void handleAccessibilityOfViewClassName(View... views) {
        for (View view : views) {
            if (view == null) {
                return;
            }
            view.setAccessibilityDelegate(new View.AccessibilityDelegate() {

                @Override
                public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
                    super.onInitializeAccessibilityNodeInfo(host, info);
                    info.setClassName("");
                }
            });
        }
    }

实践九:判断无障碍服务是否开启

有一些业务场景,需要我们判断当前是否开启无障碍,这个方法比较耗时,需要放在子线程。

  /**
    * 判断版本号
    */
  public static boolean buildVersionHeightJellyBean() {
      return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
  }
 
    /**
      * 判断是否开启了无障碍服务
      */
  public static boolean isEnableAccessibilityService(Context context) {
        if (!buildVersionHeightJellyBean()) {
            return false;
        }
        AccessibilityManager manager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
        if (manager == null || !manager.isEnabled()) {
            return false;
        }
        List<AccessibilityServiceInfo> infos = manager.getEnabledAccessibilityServiceList(1);
        if (infos == null || infos.isEmpty() || !manager.isTouchExplorationEnabled()) {
            return false;
        }
        return true;
  } 

实践十:如何让父容器可以选中,并且朗读

有一些自定义View,本身无法获取焦点,也不具备无障碍朗读功能。比如LinearLayout,RelativeLayout本身没有焦点。可通过在布局文件中添加android:focusable="true"属性和android:focusableInTouchMode="true"属性设置控件可获取焦点。也可以通过代码的方式设置,调用View的setFocusable和setFocusableInTouchMode方法,传入true。这样选中父容器时,可以朗读子控件的标签内容。

需要注意的是,对于可获取焦点的子控件,如果属性没有设置成不可获取焦点,只有控件获取焦点才朗读标签内容,选择它的容器控件时,不会朗读标签内容。

实践十一:自动循环播放的轮播图控件

针对自动循环播放的轮播图,当标签还没朗读完,翻到下一页,开始朗读下一页的标签,这种操作不是很友好。

所以针对这种控件,需首先判断无障碍是否是开启状态,开启状态,关闭循环播放,让用户可以用手指滑动,滑动到下一个图片,朗读标签。

对于无障碍开发,不仅要掌握一定的无障碍开发技能,还要注重实现效果对无障碍人士易用性和友好性。

实践十二:DrawerLayout控件查看源码发现:DrawerLayout类updateChildrenImportantForAccessibility方法,源码如下:


private void updateChildrenImportantForAccessibility(View drawerView, boolean isDrawerOpen) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if ((!isDrawerOpen && !isDrawerView(child)) || (isDrawerOpen && child == drawerView)) {
                // Drawer is closed and this is a content view or this is an
                // open drawer view, so it should be visible.
                ViewCompat.setImportantForAccessibility(child,
                        ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
            } else {
                ViewCompat.setImportantForAccessibility(child,
                        ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
            }
        }
    }

当我们使用DrawerLayout控件实现抽屉时,抽屉打开时,如果视图是抽屉支持无障碍朗读,如果视图不是抽屉则无障碍不可用。相反,抽屉关闭时,如果视图不是抽屉支持无障碍朗读,如果视图是抽屉则无障碍不可用。

设置无障碍的方法在三处调用,dispatchOnDrawerClosed,dispatchOnDrawerOpened和openDrawer三个方法中调用updateChildrenImportantForAccessibility更新无障碍可用和不可用状态。

对于setDrawerListener方法,传参数DrawerListener,DrawerListener有许多方法,其中最重要的两个方法是抽屉打开与抽屉关闭。

(1)抽屉打开的方法:void onDrawerOpened(@NonNull View drawerView);

以抽屉打开为例:方法调用关系如下:

updateDrawerState>dispatchOnDrawerOpened>onDrawerOpened,那么updateDrawerState在哪里调用呢?

updateDrawerState有3处调用,分别是:

打开抽屉  public void openDrawer(@NonNull View drawerView, boolean animate)

关闭抽屉  public void closeDrawer(@NonNull View drawerView, boolean animate)

public void onViewDragStateChanged(int state)

也就是说无论抽屉打开,关闭,状态变化都会去调用方法,更新无障碍可用状态。

(2)抽屉关闭的方法:void onDrawerClosed(@NonNull View drawerView);

void onDrawerOpened(@NonNull View drawerView);

void onDrawerClosed(@NonNull View drawerView);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值