简单使用 Accessibility 及 Android 适配 TalkBack,实现适老化和无障碍

2,558 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言:工信部发布的互联网应用适老化及无障碍改造的通知,app 需要进行改造,以提高老年人或者视障人士在使用 app 过程的便捷性,改造的内容包括但不限于

1、UI 界面更简单、整洁(界面元素不能过于复杂,字体字号需要偏大、清晰)

2、页面焦点导航的适配

3、页面元素需要适配 TalkBack 朗读

4、搭建无障碍服务 service

关于界面文字字号大小的选择的,我前面发过文章介绍过方案,今天主要讲讲 android 中 Accessibility 相关使用以及其他一些简单的改造。

一、给控件加上 contentDescription 属性和正确的朗读文案。

(1)对于 TextView / Button,TalkBack 会读出设置的 text 属性的文本,一般情况不需要特殊适配

(2)对于 EditText ,TalkBack 会读 hint 属性设置的文字,输入内容后会播报输入后的问题

(3)对于其他没有 text 属性的控件

         1、通过 layout 文件里面设置 contentDescription 属性设置文本

         2、通过代码中调用 view.setContentDescription() 方法设置文本

    <ImageView
        android:layout_width="@dimen/dp_190"
        android:layout_height="@dimen/dp_190"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:src="@drawable/bg_login_watermark"
        android:contentDescription="@string/love_version_read_Img_Water_mark"/>

iv_back.setContentDescription(getString(R.string.love_version_read_Img_Back));

二、启用焦点导航

在使用 TalkBack 测试的过程中,发现有些比较复杂的布局内容子控件点击不到,因此也无法播报,这就需要我们对页面的焦点视图做好控制。

(1)如果我们需要某个控件是可以在 TalkBack 模式下可点击并播报的话,需要给控件设置 android:focusable = "true",或者在代码中调用 setFocusable()。否则的话焦点会在整个父布局,导致某个区域块点击会顺序播报整块区域的内容。

(2)设置焦点顺序。

        通过 android:nextFocusDown, android:nextFocusLeft, android:nextFocusRight, android:nextFocusUp 或者运行时通过 setNextFocusDownId(), setNextFocusrightId() 等方法动态控制用户界面组件的聚焦顺序。以确保用户在使用手势、虚拟键导航时可以有较好的体验。

(3)talkback 开启不获取焦点。

        如果你希望该区域(ViewGroup、View)不播报,通过下面代码来设置。

<View 
android:impoartantForAccessibility="no"/> 

三、给控件加上正确的播报类型

一般的,我们需要让视障人士清楚当前点击的区域是什么,比如我们的按钮、图片等等,开启 talkback 的情况下,通过设置类型来让系统在控件获取焦点时播报。

例如:下面的设置,系统将会播报“按钮,(设置好的 contentDescription)”

ViewCompat.setAccessibilityDelegate(tv_url, new AccessibilityDelegateCompat(){
            @Override
            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
                super.onInitializeAccessibilityNodeInfo(host, info);
                info.setClassName(Button.class.getName());
            }
        });

四、拦截自定义视图的播报。

项目中,难免会用到一些自定义的组件,比如 banner、手势滑动组件等等。例如在项目中我自已写的一个画廊效果的滑动窗,在开启 talkback 模式之后,滑动会播报 “多视图页面,第一项,第二项”之类的,我们当然不希望用户听到这些,即便是在加了 contentDescription 之后也依旧会存在,这个时候,通过重写 onRequestSendAccessibility() 方法,你可以直接 return false,即不处理该事件,就不会播报上述的“多视图页面”。

@Override
    public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
        return false;
    }

五、拦截页面的播报。

开启 talkback 模式下,在 Activity 或者 Fragment中,进入一个新的页面时,往往会触发系统一些播报。我遇到的情况是,在首页有广告 banner 的情况下,进入首页系统就会播报“第 2000 项目,共 两千零几项”这种。

解决方法:

@Override
    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
        event.getText().add("");
        return super.dispatchPopulateAccessibilityEvent(event);
    }

六、主动播报。

有些场景下,需要对用户的滑动做出反馈,比如 banner,滑动选择窗口。当用户滑动到某一项,但是还未点击的时候,我们可以做出反馈,通过 announceForAccessibility() 方法 及时反馈当前的选中项。

tv_url.announceForAccessibility("您当前选中的是。。。");

七、判断当前是否开启 talkback 模式。

有些场景,需要判断手机是否开启 talkback 模式,以此来进行一些特殊的逻辑处理。比如一些app会有人脸识别、身份证识别的功能。在talkback 模式下,对当前的识别状态进行播报反馈,提升用户的体验。比如人脸识别,脸太靠前,或者不居中等情况,一般都只有文字提示,我们判断系统是否开启 talkback,将文字提示进行播报。

(1)通过 AccessibilityManager 的 isEnabled() 方法。

AccessibilityManager accessibilityManager;
accessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
boolean isTalkBackEnabled = accessibilityManager.isEnabled();

但是这个方法有个问题,就是不够准确,测试的时候发现,有的手机单用这个方法会有误判的情况,就是手机明明没有开启 talkback,但还是返回了 true。后面了解到是有些 app ,会开启相关的 AccessibilityService ,比如爱奇艺,所以在安装了爱奇艺app的手机,isTalkBackEnabled 就返回了 true。影响了我们的判断结果。所以我们需要再加一个判断。

(2)直接上代码了

private static boolean isTalkBackEnable(Context context) {

        Intent screenReaderIntent = new Intent(SCREEN_READER_INTENT_ACTION);
        screenReaderIntent.addCategory(SCREEN_READER_INTENT_CATEGORY);
        List<ResolveInfo> screenReaders = context.getPackageManager().queryIntentServices(screenReaderIntent, 0);
        
        if (screenReaders == null || screenReaders.size() <= 0) {
            return false;
        }

        boolean hasActiveScreenReader = false;
        if (Build.VERSION.SDK_INT <= 15) {
            ContentResolver cr = context.getContentResolver();
            Cursor cursor = null;
            int status = 0;

            for (ResolveInfo screenReader : screenReaders) {
                cursor = cr.query(Uri.parse("content://" + screenReader.serviceInfo.packageName
                        + ".providers.StatusProvider"), null, null, null, null);

                if (cursor != null && cursor.moveToFirst()) { 
                    status = cursor.getInt(0);
                    cursor.close();
                    // 状态1为开启状态,直接返回true即可
                    if (status == 1) {
                        return true;
                    }
                }
            }
        } else if (Build.VERSION.SDK_INT >= 26) {
            // 高版本可以直接判断服务是否处于开启状态
            for (ResolveInfo screenReader : screenReaders) {
                hasActiveScreenReader |= isAccessibilitySettingsOn(context, screenReader.serviceInfo.packageName + "/" + screenReader.serviceInfo.name);
            }

        } else {
            // 判断正在运行的Service里有没有上述存在的Service
            List<String> runningServices = new ArrayList<String>();

            android.app.ActivityManager manager = (android.app.ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
            for (android.app.ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
                runningServices.add(service.service.getPackageName());
            }

            for (ResolveInfo screenReader : screenReaders) {
                if (runningServices.contains(screenReader.serviceInfo.packageName)) {
                    hasActiveScreenReader |= true;
                }
            }
        }

        return hasActiveScreenReader;
    }

以上两点同时使用就可以准确判断是否开启 talkback 了。第二点是参考下面这篇博客的,感谢老哥哈,一开始我只用了第一种,在测试机就没问题,在自己手机就不行,后面看完才知道是我手机安装了爱奇艺的原因。

参考的博客地址:Android完美判断是否开启无障碍服务功能 - 简书

~~ 这是一条分隔线~~                                                                   

本次 APP 适老化改造用到的内容大概就这么多,后面如果有继续优化再接着补充。