Android无障碍浅析

作者: 王岩_shang | 来源:发表于2017-05-29 16:20 被阅读2317次

    前言

    Android无障碍,我们平常接触时,比较熟悉的有“绿色守护”以及“抢红包”这些,其便利性便是在没有“root权限”的情况下,可以“触摸”其他应用来做一些操作。然而,无障碍的初衷却是为视觉障碍的人提供操控手机的可能。

    Android中的实现

    Android包含几个支持视觉障碍用户访问的特性;他们不需要应用做出巨大的视觉改变。
    TalkBack是由Google公司提供的一个预安装屏幕阅读服务。它使用语音反馈描述操作的结果(如启动一个app)和事件(如通知)。
    Explore by Touch(触摸浏览)是与TalkBack协作的系统特性,允许用户触摸设备屏幕并通过语音反馈听取手指触摸的内容。该特性对低视力用户有帮助。
    无障碍设置允许用户修改设备的展示和声音选择,例如放大文本字体,改变文本阅读的速度等等。
    一些用户使用硬件或软件定向控制(例如,D-pad,轨迹球,键盘)从屏幕上的一个选择跳转到另一个选择。他们以线性顺序与应用的结构进行交互,这种线性顺序类似于电视的四个方向远程控制导航。

    基本组件的无障碍开发

    对于Android的基础组件,只需要简单的在xml或代码中设置contentDescription属性。

    • AccessibilityEvent : 在用户和ui交互时,由系统发送的无障碍事件,例如按钮被点击,或者一个view被focus,参见AccessibilityService,一个无障碍事件的最主要作用就是暴露给AccessibilityService足够多的信息,以提供给用户界面良好的反馈。

    • AccessibilityNodeInfo:一个view状态的快照,代表了窗口中包含的节点的信息。

    • View.AccessibilityDelegate:View 的内部类,通过组合而非继承的方式来控制处理无障碍事件。包括发送,初始化事件以及节点属性。

    辣么我们来看下view里面是如何初始化无障碍事件的:

    /**
         * Initializes an {@link AccessibilityEvent} with information about
         * this View which is the event source. In other words, the source of
         * an accessibility event is the view whose state change triggered firing
         * the event.
         * <p>
         * Example: Setting the password property of an event in addition
         *          to properties set by the super implementation:
         * <pre> public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
         *     super.onInitializeAccessibilityEvent(event);
         *     event.setPassword(true);
         * }</pre>
         * <p>
         * If an {@link AccessibilityDelegate} has been specified via calling
         * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
         * {@link AccessibilityDelegate#onInitializeAccessibilityEvent(View, AccessibilityEvent)}
         * is responsible for handling this call.
         * </p>
         * <p class="note"><strong>Note:</strong> Always call the super implementation before adding
         * information to the event, in case the default implementation has basic information to add.
         * </p>
         * @param event The event to initialize.
         *
         * @see #sendAccessibilityEvent(int)
         * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent)
         */
        @CallSuper
        public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
            if (mAccessibilityDelegate != null) {
                mAccessibilityDelegate.onInitializeAccessibilityEvent(this, event);
            } else {
                onInitializeAccessibilityEventInternal(event);
            }
        }
    
        /**
         * @see #onInitializeAccessibilityEvent(AccessibilityEvent)
         *
         * Note: Called from the default {@link AccessibilityDelegate}.
         *
         * @hide
         */
        public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
            event.setSource(this);
            event.setClassName(getAccessibilityClassName());
            event.setPackageName(getContext().getPackageName());
            event.setEnabled(isEnabled());
            event.setContentDescription(mContentDescription);
    
            switch (event.getEventType()) {
                case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
                    ArrayList<View> focusablesTempList = (mAttachInfo != null)
                            ? mAttachInfo.mTempArrayList : new ArrayList<View>();
                    getRootView().addFocusables(focusablesTempList, View.FOCUS_FORWARD, FOCUSABLES_ALL);
                    event.setItemCount(focusablesTempList.size());
                    event.setCurrentItemIndex(focusablesTempList.indexOf(this));
                    if (mAttachInfo != null) {
                        focusablesTempList.clear();
                    }
                } break;
                case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
                    CharSequence text = getIterableTextForAccessibility();
                    if (text != null && text.length() > 0) {
                        event.setFromIndex(getAccessibilitySelectionStart());
                        event.setToIndex(getAccessibilitySelectionEnd());
                        event.setItemCount(text.length());
                    }
                } break;
            }
        }
    

    在初始化调用onInitializeAccessibilityEvent时,会将设置到view中的mContentDescription等属性放进AccessibilityEvent中去。
    无障碍事件如何发送呢?

     /**
         * Sends an accessibility event of the given type. If accessibility is
         * not enabled this method has no effect. The default implementation calls
         * {@link #onInitializeAccessibilityEvent(AccessibilityEvent)} first
         * to populate information about the event source (this View), then calls
         * {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)} to
         * populate the text content of the event source including its descendants,
         * and last calls
         * {@link ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)}
         * on its parent to request sending of the event to interested parties.
         * <p>
         * If an {@link AccessibilityDelegate} has been specified via calling
         * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
         * {@link AccessibilityDelegate#sendAccessibilityEvent(View, int)} is
         * responsible for handling this call.
         * </p>
         *
         * @param eventType The type of the event to send, as defined by several types from
         * {@link android.view.accessibility.AccessibilityEvent}, such as
         * {@link android.view.accessibility.AccessibilityEvent#TYPE_VIEW_CLICKED} or
         * {@link android.view.accessibility.AccessibilityEvent#TYPE_VIEW_HOVER_ENTER}.
         *
         * @see #onInitializeAccessibilityEvent(AccessibilityEvent)
         * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent)
         * @see ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)
         * @see AccessibilityDelegate
         */
        public void sendAccessibilityEvent(int eventType) {
            if (mAccessibilityDelegate != null) {
                mAccessibilityDelegate.sendAccessibilityEvent(this, eventType);
            } else {
                sendAccessibilityEventInternal(eventType);
            }
        }
    

    在AccessibilityService中调用view 的sendAccessibilityEvent,由view中的内部类对象AccessibilityDelegate来处理。
    这样就完成了一个完整的处理流程,从初始化->用户接触产生并发送事件->接受事件->talkback和 Explore by Touch 反馈给用户。
    这里有个问题,我们没有设置过TextView中的contentDesciption属性,为什么开启无障碍后,居然能够读出上面的文字呢?

    自定义view的无障碍开发

    在做自定义view的开发时,会遇到一个问题,我们知道继承View时,此时一个单独的contentDescription是不能够描述当前的布局属性的,来给无障碍很好的反馈支持,最典型的栗子便是月历这个自定义view,单独设置contentDescription时,我们只能知道它是一个月历显示,不能知道里面的每一个节点的具体信息,星期几?几号?
    Android 便提供了一种解决方案:** 既然不是真实存在的,就虚拟出节点来。**

    • AccessibilityNodeProvider: This class is the contract a client should implement to enable support of a virtual view hierarchy rooted at a given view for accessibility purposes. A virtual view hierarchy is a tree of imaginary Views that is reported as a part of the view hierarchy when an AccessibilityService
      explores the window content. Since the virtual View tree does not exist this class is responsible for managing the AccessibilityNodeInfo
      s describing that tree to accessibility services.

    进一步的,为了降低开发成本,google为开发者提供了ExploreByTouchHelper来降低开发成本。
    整个过程大致分三个步骤:
    1 . 初始化

     mAccessHelper = new MyExploreByTouchHelper(someView);
     ViewCompat.setAccessibilityDelegate(someView, mAccessHelpe
    

    2 . 处理以及发送事件

        @Override
         public boolean dispatchHoverEvent(MotionEvent event) {
           return mHelper.dispatchHoverEvent(this, event)
               || super.dispatchHoverEvent(event);
         }
    
    sendEventForVirtualView(int, int))(int virtualViewId, int eventType)
    Populates an event of the specified type with information about an item and attempts to send it up through the view hierarchy.
    

    3 . 生成虚拟节点以及描述信息初始化

     class MyExploreByTouchHelper extends ExploreByTouchHelper{
            public MyExploreByTouchHelper(View forView) {
                super(forView);
            }
    
            @Override
            protected int getVirtualViewAt(float x, float y) {
                //根据想x,y坐标点返回虚拟节点的id
                return 0;
            }
    
            @Override
            protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
                //虚拟节点的id list
            }
    
            @Override
            protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent accessibilityEvent) {
                //填充无障碍事件的属性,例如contentDescription
                accessibilityEvent.setContentDescription(getItemDescription(virtualViewId));
    
            }
    
            @Override
            protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat accessibilityNodeInfoCompat) {
                //初始化虚拟节点的位置等其他属性
                accessibilityNodeInfoCompat.setBoundsInParent(mTempRect);
                accessibilityNodeInfoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
    
            }
    
            @Override
            protected boolean onPerformActionForVirtualView(int i, int i1, Bundle bundle) {
                return false;
            }
        }
    

    查看

    虚拟的节点如何能 查看到呢?

    Android Studio ->Tools -> Android ->Android Device Monitor -> touch Process (选中某个界面进程) -> Dump View Hierarhchy for UI Automator。
    

    这里dump 出来的不止真实的ui组件,还包括我们自建的虚拟的veiw树,这样更具象化,可以很好的定位问题。

    总结

    我们在做日常开发的过程中,很少会关注到无障碍这部分的内容,但是仍有一部分人也希望能够和我们一样方便的使用移动设备,所以也希望开发者们能够关注到这一点,虽然很小的一点改变,却能让这个世界更美好一点。
    也有部分开发者通过创新,希望能够利用到contentDescription字段做一些协议层面的东西,毕竟Android框架提供了这个不需要root权限变可以通过一个app来做跨应用服务。但是这里可能也需要权衡,毕竟有一部分人是需要【初衷】的无障碍。

    参考

    相关文章

      网友评论

        本文标题:Android无障碍浅析

        本文链接:https://www.haomeiwen.com/subject/yxukfxtx.html