盲人辅助模式Accessibility剖析

现在很多的App都有考虑到要适配一些特殊人群,不如盲人,对于盲人模式这个辅助功能的适配,参考Google官方文档:http://developer.android.com/intl/zh-cn/guide/topics/ui/accessibility/apps.html ,下面是我在研究盲人模式开发中的一些总结,希望对有需要的人能够起到一定的帮助作用。

一、常见的需求场景

1、描述用户界面控件:为没有可见文本的用户界面控件提供内容描述,尤其是ImageButton,ImageView 和CheckBox组件。使用android:contentDescription XML layout属性或者setContentDescription(CharSequence)方法来为无障碍服务提供信息(装饰性图形除外)
2、设置焦点导航:确保用户可以使用基于硬件或软件的定向控制(D-垫,轨迹球,键盘和导航手势)导航。在某些情况下,需要设置UI组件为可获取焦点,或者设置焦点的跳跃顺序更合用户操作逻辑
3、自定义视图控件:若要为应用建立自定义界面控件,须为自定义视图设置无障碍界面且提供内容描述。
4、不使用纯音频的反馈:音频反馈必须有一个次级反馈机制,确保语音提示总是伴随着另一种视觉提示或通知, 来为失聪或者听力障碍的用户提供支持。例如,短信接收的提示音必须伴随一个系统提示,触觉反馈(如具备条件)或者视觉提示。

二、一些特殊情况和注意事项

盲人模式适配在开发过程中可能会遇到一些特殊的情况,应用开发应该具体情况具体分析,并选择合适的技术处理,这里总结一些比较常见的注意事项:
1、文本域提示:对于EditText域,提供一个android:hint属性而不是内容描述,来帮助用户理解,当文本域为空时,期望填写什么内容。且当内容填充时,允许语音系统以音频形式播放内容。
2、高可视化语境的自定义控件:如果应用包含高可视化语境的自定义控件(如日历控件),默认的无障碍服务不能为用户提供良好的体验,应该考虑使用AccessibilityNodeProvider为控件提供虚拟的视图层。虚拟视图层怎么提供要具体情况具体分析,可以参考这篇博客文章 http://www.programcreek.com/java-api-examples/index.php?api=android.view.accessibility.AccessibilityNodeProvider
3、自定义控件和点击事件处理:如果自定义控件提供用户触摸交互的处理,例如onTouchEvent(MotionEvent)对MotionEvent.ACTION_DOWN和MotionEvent.ACTION_UP的监听,并将之视为一次点击事件。需要触发一个AccessiblityEvent,等同于一次点击事件并为用户行为提供无障碍服务。具体参考下面自定义触摸事件处理。
4、改变功能的控件:如果应用中有button或者其他在正常用户行为中能改变功能的控件(例如,button从play变成了pause),确保button的android:contentDescription也对应做了改变。
5、为相关控件提供提示:当用户与控件集中的控件(如DatePicker)交互时,确保控件集提供有效的音频反馈。
6、视频播放与字幕:如果应用提供视频播放功能,应该支持字幕以帮助失聪或者听力障碍的用户理解信息。视频播放控件也必须清楚得表明字幕是否可用,并提供一个明确的方法以启用字幕。
7、增补无障碍音频反馈:使用安卓的无障碍框架为应用提供无障碍音频反馈。如TalkBack这样的无障碍服务应该是为应用提供无障碍视频提示的唯一服务。使用android:contentDescription XML layout属性或者使用无障碍框架API动态增加来提供提示信息。例如,如果你想通过声音控制应用程序,如使用announceForAccessibility(CharSequence)方法,获取辅助性服务,当用户说出信息时,实现自动翻转书页功能。
8、复杂视觉交互的自定义控件:对于提供复杂视觉交互或者非标准视觉交互的自定义控件,使用可为无障碍服务提供简单交互模型的AccessibilityNodeProvider为用户操作提供一个虚拟的视觉层。如果这种方法不可行,考虑提供一个不同的可访问的视觉层。
9、装饰性的图像与图形:应用屏幕上只为装饰的且不提供内容或者可供用户操作的元素(比如自己画的线或者小图案这些装饰),不应该有无障碍内容描述。

三、自定义视图及处理自定义的触摸事件

如果应用程序需要一个自定义的视图组件,需要做一些额外的工作来确保自定义视图是支持辅助功能:
1、处理方向控制器点击
2、实现accessibility API 的方法(Eg:sendAccessibilityEvent() ,dispatchPopulateAccessibilityEvent(),onInitializeAccessibilityEvent(),onInitializeAccessibilityNodeInfo()等等)
3、发送自定义视图特定的AccessibilityEvent对象
4、为自定义视图填充AccessibilityEvent和AccessibilityNodeInfo对象

接下来介绍一下自定义触摸事件的处理:

1、自定义视图控件可能需要非标准的触摸事件的行为。例如,一个自定义控件可以使用onTouchEvent(MotionEvent)来侦测ACTION_DOWN 和 ACTION_UP事件,触发特殊的单击事件。为了用一个 有效的方法来处理这些需求,代码应该重写performClick()方法,该方法必须调用超类方法的实现,然后执行任何需要通过点击事件完成的操作。当检测到自定义点击击动作,代码调performClick()方法。下面的代码片段来自google官方文档。

class CustomTouchView extends View {

    public CustomTouchView(Context context) {
        super(context);
    }

    boolean mDownTouch = false;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        // Listening for the down and up touch events
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownTouch = true;
                return true;

            case MotionEvent.ACTION_UP:
                if (mDownTouch) {
                    mDownTouch = false;
                    performClick(); // Call this method to handle the response, and
                                    // thereby enable accessibility services to
                                    // perform this action for a user who cannot
                                    // click the touchscreen.
                    return true;
                }
        }
        return false; // Return false for other touch events
    }

    @Override
    public boolean performClick() {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any
        super.performClick();

        // Handle the action for the custom click here

        return true;
    }
}
 注意:
       Note: If your custom view has distinct clickable regions, such as a custom calendar view, you must implement a virtual view hierarchy by overriding getAccessibilityNodeProvider() in your custom view in order to be compatible with accessibility services.

2、可能还会遇到这样子的一种情况,在talkback模式下触摸事件被上层View消费掉,没有传到下面具体的子view上,类似密码锁在talkback模式上无法输入密码这种问题,遇到类似的问题可以重写onHoverEvent(MotionEvent event)对事件进行分发处理。核心代码片段:

 @Override
        public boolean onHoverEvent(MotionEvent event) {
            AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
            if (accessibilityManager != null && accessibilityManager.isTouchExplorationEnabled()) {
                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;
                }
                onTouchEvent(event);
                event.setAction(action);
            }
            return super.onHoverEvent(event);
        }

四、AccessibilityEvent事件类型

这里列出AccessibilityEvent的所有事件类型(Mask for {@link AccessibilityEvent} all types),具体可以参考AccessibilityEvent.java源码,里面有所有事件类型的介绍。

 /**
     * Represents the event of clicking on a {@link android.view.View} like
     * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc.
     */
    public static final int TYPE_VIEW_CLICKED = 0x00000001;

    /**
     * Represents the event of long clicking on a {@link android.view.View} like
     * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc.
     */
    public static final int TYPE_VIEW_LONG_CLICKED = 0x00000002;

    /**
     * Represents the event of selecting an item usually in the context of an
     * {@link android.widget.AdapterView}.
     */
    public static final int TYPE_VIEW_SELECTED = 0x00000004;

    /**
     * Represents the event of setting input focus of a {@link android.view.View}.
     */
    public static final int TYPE_VIEW_FOCUSED = 0x00000008;

    /**
     * Represents the event of changing the text of an {@link android.widget.EditText}.
     */
    public static final int TYPE_VIEW_TEXT_CHANGED = 0x00000010;

    /**
     * Represents the event of opening a {@link android.widget.PopupWindow},
     * {@link android.view.Menu}, {@link android.app.Dialog}, etc.
     */
    public static final int TYPE_WINDOW_STATE_CHANGED = 0x00000020;

    /**
     * Represents the event showing a {@link android.app.Notification}.
     */
    public static final int TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040;

    /**
     * Represents the event of a hover enter over a {@link android.view.View}.
     */
    public static final int TYPE_VIEW_HOVER_ENTER = 0x00000080;

    /**
     * Represents the event of a hover exit over a {@link android.view.View}.
     */
    public static final int TYPE_VIEW_HOVER_EXIT = 0x00000100;

    /**
     * Represents the event of starting a touch exploration gesture.
     */
    public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200;

    /**
     * Represents the event of ending a touch exploration gesture.
     */
    public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400;

    /**
     * Represents the event of changing the content of a window and more
     * specifically the sub-tree rooted at the event's source.
     */
    public static final int TYPE_WINDOW_CONTENT_CHANGED = 0x00000800;

    /**
     * Represents the event of scrolling a view.
     */
    public static final int TYPE_VIEW_SCROLLED = 0x00001000;

    /**
     * Represents the event of changing the selection in an {@link android.widget.EditText}.
     */
    public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000;

    /**
     * Represents the event of an application making an announcement.
     */
    public static final int TYPE_ANNOUNCEMENT = 0x00004000;

    /**
     * Represents the event of gaining accessibility focus.
     */
    public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000;

    /**
     * Represents the event of clearing accessibility focus.
     */
    public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000;

    /**
     * Represents the event of traversing the text of a view at a given movement granularity.
     */
    public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 0x00020000;

    /**
     * Represents the event of beginning gesture detection.
     */
    public static final int TYPE_GESTURE_DETECTION_START = 0x00040000;

    /**
     * Represents the event of ending gesture detection.
     */
    public static final int TYPE_GESTURE_DETECTION_END = 0x00080000;

    /**
     * Represents the event of the user starting to touch the screen.
     */
    public static final int TYPE_TOUCH_INTERACTION_START = 0x00100000;

    /**
     * Represents the event of the user ending to touch the screen.
     */
    public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000;

    /**
     * Represents the event change in the windows shown on the screen.
     */
    public static final int TYPE_WINDOWS_CHANGED = 0x00400000;

    /**
     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
     * The type of change is not defined.
     */
    public static final int CONTENT_CHANGE_TYPE_UNDEFINED = 0x00000000;

    /**
     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
     * A node in the subtree rooted at the source node was added or removed.
     */
    public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001;

    /**
     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
     * The node's text changed.
     */
    public static final int CONTENT_CHANGE_TYPE_TEXT = 0x00000002;

    /**
     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
     * The node's content description changed.
     */
    public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004;

五、例子说明

1、我之前做过的适配
(1)创建自定义控件视图(控件要视具体情况而定,复杂情况需要处理事件的分发等)

/**
 * 由于原生的checkbox已经写死TalkBack的声音提示,重写setAccessibilityDelegate()设置提示文本,最终还是会被原生覆盖
 * 所以需要自定义checkbox重写onInitializeAccessibilityNodeInfo设置自己的文本声音提示
 */
public class SosCheckBox extends CheckBox  {
     private boolean mIsAccessibilityEnable = false;
    public SosCheckBox(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        info.setCheckable(false);
        info.setPackageName(null);
        info.setClassName(null);
        info.setText(null);
    }

    @Override
    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(event);
      //当事件为单点的事件,就是一只手指点下去响应的事件
        if(event.getEventType() == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER){ 
            if(isChecked()){
                setContentDescription(getResources().getString(R.string.sos_open_state));
            }else{
                setContentDescription(getResources().getString(R.string.sos_close_state));
            }
        }
    }
}

(2)在Activity中初始化服务并实现语音播报(这里要根据自己具体情况具体分析,根据自己的业务逻辑来处理)

//当前状态是否支持accessibility
 protected void onResume() {
        super.onResume();
        //当前状态是否支持accessibility
        AccessibilityManager accessibilityManager = (AccessibilityManager) this.getSystemService(Context.ACCESSIBILITY_SERVICE);
        mIsAccessibilityEnable = accessibilityManager.isEnabled();
    }

 /** 
     * 发送Accessibility事件
     * @param host 目标View
     * @param eventType 事件类型
     * @param info 事件信息
     */
    public void sendAccessibilityEvent(View host, int eventType, String info) {
        if (!mIsAccessibilityEnable) {
            return;
        }
        if (host == null) {
            return;
        }
        host.setContentDescription(info);
        host.sendAccessibilityEvent(eventType);
    }
}

//然后在应用相对应的地方调用此方法进行语音播报。
sendAccessibilityEvent(mSosCheckBox, AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION,  getString(R.string.sos_open));

2.例如 屏幕锁屏状态下朗读锁屏密码
(1)处理具体逻辑

//是否朗读密码
private boolean shouldSpeakPasswordForAccessibility() {
    final boolean speakPassword = Settings.Secure.getInt(
            getContext().getContentResolver(), Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0;
    final boolean hasHeadphones = mAudioManager != null ?
            (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())
            : false;
    return speakPassword || hasHeadphones;
}

//然后在addCellToPattern(Cell newCell)方法中加上要朗读的内容
private void addCellToPattern(Cell newCell) {
    if (android.os.Build.VERSION.SDK_INT > 16 && shouldSpeakPasswordForAccessibility()) {
        int num = newCell.getRow() * 3 + newCell.getColumn() + 1;
        if (num == 11) {
            num = 0;
        }
        announceForAccessibility(num + "");//当API大于16,就可以用这个方法播报语音内容
    }
    mPattern.add(newCell);
    notifyCellAdded();
}

(2)初始化AccessibilityService服务

public LockDigitView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
    }

(3)由于密码锁事件没有传递到具体的“数字”子view上就会被上层view消费掉,所以这里需要做事件的处理

@Override
    public boolean onHoverEvent(MotionEvent event) {
        AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
        if (accessibilityManager != null && accessibilityManager.isTouchExplorationEnabled()) {
            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;
            }
            onTouchEvent(event);
            event.setAction(action);
        }
        return super.onHoverEvent(event);
    }

Ok,到这里屏幕锁屏密码播报就已经适配结束

3.来个简单的例子,搜索框
搜索框其实就是一个布局文件(mc_search_layout.xml)都是用基本控件组成,所以直接在相应组件上加android:contentDescription就可以了。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/search_layout">
    <ImageView
        android:id="@+id/search_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:scaleType="centerInside"
        android:contentDescription="@string/search_icon"/>

    <ImageView
        android:id="@+id/search_icon_input_clear"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:scaleType="centerInside"
        android:contentDescription="@string/search_icon_input_clear"
        android:visibility="gone" />
</RelativeLayout>
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值