分享

Android无障碍宝典

 树悲风 2016-08-04

作者简介: 何金源,腾讯Android手Q开发工程师,负责Android手Q无障碍优化工作,对Android无障碍系统原理及开发技术有深入了解。 


Android江湖上一直流传着一部秘籍——Android无障碍宝典。传闻练成这部宝典,可在Android无障碍模式下,飞檐走壁,能人所不能。宝典分为三篇,分别是入门、进阶和高级,由浅入深,全面展示无障碍的基本方法及扩展应用。


Android应用无障碍化,目的是为视觉障碍或其他有障碍的用户提供更好的服务。在无障碍模式下,用户的操作方式与平常不同,比如:


  • 选择(Hover)一个元素:单击 

  • 点击(Click)一个元素:双击 

  • 滚动:双指往上、下、左、右 

  • 选择上或下一个项目:单指往上、下、左、右 

  • 快速回到主画面:单指上滑+左滑 

  • 返回键:单指下滑+左滑 

  • 最近画面键:单指左滑+上滑 

  • 通知栏:单指右滑+下滑


此外,还需要理解在无障碍模式下“无障碍焦点”这个概念。如图1所示,界面上以绿色方框来表示目前获得无障碍焦点的View。拥有无障碍焦点的View,会被TalkBack服务识别,TalkBack会从View中取出相关的无障碍内容,然后提示给用户。


图1 无障碍焦点


有了对无障碍模式初步的了解,就可以正式开始学习如何为应用无障碍化。


入门篇


为View添加ContentDescription


UI上的可操作元素都应该添加上ContentDescription, 当此元素获得无障碍焦点时,TalkBack服务就取出View的提示语(contentDescription),并朗读出来。


添加ContentDescription有两种方法,第一种是通过在XML布局中设置android:contentDescripton属性,如:


 tton> 


但是很多情况下,View的内容描述会根据不同情景需要而改变,比如CheckBox按钮是否被选中,以及ListView中item的内容描述等。这种则需要在代码中使用setContentDescription方法,如:


String contentDescription = '已选中 ' + strValues[position];

label.setContentDescription(contentDescription);


设置无障碍焦点


UI上的元素,有的默认带有无障碍焦点,如Button、CheckBox等标准控件,有的如果不设置contentDescription是默认没有无障碍焦点。在开发应用过程中,还会遇到一些UI元素,是不希望它获取无障碍焦点的。以下方法可以改变元素的无障碍焦点:


public void setAccessibilityFocusable(View view, boolean focused){

    if(android.os.Build.VERSION.SDK_INT >= 16){

        if(focused){

            ViewCompat.setImportantForAccessibility(view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);

        }else{

            ViewCompat.setImportantForAccessibility(view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);

        }

    }

}


IMPORTANT_FOR_ACCESSIBILITY_YES表示这个元素应该有无障碍焦点,会被TalkBack服务读出描述内容;IMPORTANT_FOR_ACCESSIBILITY_NO表示屏蔽元素的无障碍焦点,手指滑动遍历及触摸此元素,都不会获得无障碍焦点,TalkBack服务也不会读出其描述内容。


发出无障碍事件


IMPORTANT_FOR_ACCESSIBILITY_YES表示这个元素应该有无障碍焦点,会被TalkBack服务读出描述内容;IMPORTANT_FOR_ACCESSIBILITY_NO表示屏蔽元素的无障碍焦点,手指滑动遍历及触摸此元素,都不会获得无障碍焦点,TalkBack服务也不会读出其描述内容。


view.postDelayed(new Runnable() {

    @Override

    public void run() {

        if(android.os.Build.VERSION.SDK_INT >= 14){

            view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);

        }

    }

},100);


这个方法是让View来自动发出被单击选中的无障碍事件,发出后,UI上的无障碍焦点则会马上赋给这个View,从而达到抢无障碍焦点的效果。再比如:


if(android.os.Build.VERSION.SDK_INT >= 16){

    AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT);

    event.setPackageName(view.getContext().getPackageName());

    event.setClassName(view.getClass().getName());

    event.setSource(view);

    event.getText().add(desc);

    view.getParent().requestSendAccessibilityEvent(view, event);

}


AccessibilityEvent.TYPE_ANNOUNCEMENT是代表元素需要TalkBack服务来读出描述内容。其中desc是描述内容,将它放到event的getText()中,然后请求View的父类来发出事件。


进阶篇


介绍AccessibilityDelegate


Android中View含有AccessibilityDelegate这个子类,它可被注册进View中,主要作用是为了增强对无障碍化的支持。


查看View的源码可发现,注册Accessibility Delegate方法很简单:


Public void setAccessibilityDelegate(AccessibilityDelegate delegate) {

    mAccessibilityDelegate = delegate;

}


注册后,View对无障碍的处理,则会交给AccessibilityDelegate,如:


public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {

    if (mAccessibilityDelegate != null) {

        mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(this, info);

    } else {

        onInitializeAccessibilityNodeInfoInternal(info);

    }

}


onInitializeAccessibilityNodeInfo是View源码中初始化无障碍节点信息的方法,从上面代码看出,当mAccessibilityDelegate是开发注册的AccessiblityDelegate时,则会执行AccessiblityDelegate中的onInitializeAccessibilityNodeInfo方法。再看看AccessibilityDelegate类中的onInitializeAccessibilityNodeInfo:


public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {

    host.onInitializeAccessibilityNodeInfoInternal(info);

}


Host是被注册AccessibilityDelegate的View,onInitializeAccessibilityNodeInfoInternal是View中真正初始化无障碍节点信息的方法。即是说,注册了AccessibilityDelegate并没有改变View原来对无障碍的操作,而是在这个操作之后增加了处理。


AccessibilityDelegate的应用


下面介绍下AccessibilityDelegate可以提供哪些无障碍应用,注册AccessibilityDelegate是在API 14以上才开放的接口,API 14以下如需使用,可以接入support v4包中的AccessibilityDelegateCompt。注册方法如下:


if (Build.VERSION.SDK_INT >= 14) {

     View view = findViewById(R.id.view_id);

     view.setAccessibilityDelegate(new AccessibilityDelegate() {

         public void onInitializeAccessibilityNodeInfo(View host,

                 AccessibilityNodeInfo info) {

             super.onInitializeAccessibilityNodeInfo(host, info);

             // 对info做出扩展性支持

     });

 }


要对info做出扩展支持,还得先了解AccessibilityNodeInfo这个类。Android开发都知道,UI上的元素是通过View来实现,而AccessibilityNodeInfo则是存储View的无障碍信息(如contentDescription)及无障碍状态(如focusable、visiable、clickable等),同时它还肩负着TalkBack服务和View之间通讯的桥梁作用。如果要修改View的无障碍提示,比如修改View的类型提示,可以这样做:


if(android.os.Build.VERSION.SDK_INT >= 14){

    view.setAccessibilityDelegate(new AccessibilityDelegate(){

        @Override

        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {

            super.onInitializeAccessibilityNodeInfo(host, info);

            if(contentDesc != null) {

                info.setContentDescription(contentDesc);

            }

            info.setClassName(className);

        }

    });

}


className是类型名称,这里如果是Button.class.getName(),则TalkBack会对这个View读“XXX 按钮”,“XXX”是contentDescription,而“按钮”则是TalkBack服务添加的(如果是英文环境,则是“XXX button”)。使用上面的方法,可以为非按钮控件加上“按钮”的提示,方便无障碍用户识别UI上元素的作用,同时又不必把“按钮”提示强加入contentDescription中。除了修改AccessibilityNodeInfo外,使用AccessibilityDelegate还可以影响无障碍事件,如:


if (android.os.Build.VERSION.SDK_INT >= 14) {

    view.setAccessibilityDelegate(new AccessibilityDelegate() {


        @Override

        public void sendAccessibilityEvent(View host, int eventType) {

    // 弹出Popup后,不自动读各项内容

            if (eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {

                super.sendAccessibilityEvent(host, eventType);

            }

        }

    });

}


无障碍模式下,弹出Dialog,则会把Dialog中的所有元素都读一遍。这个方法可以把弹起窗口的无障碍事件拦截,Dialog弹起就不会再自动读各项内容。


高级篇


有了AccessibilityDelegate这把利器之后,开发可以轻松对应Android中大部分的无障碍化,但是如果想要做到游刃有余,还得深造更高级的功夫——自定义View无障碍化。


应用开发过程中,总会需要自定义View来实现特殊的UI效果,当一个自定义View中包含多种UI元素时,无障碍模式下并不能区分包含的多种UI元素,而只为自定义View添加一个大无障碍焦点。如图2所示。


图2 自定义View只有一个大无障碍焦点


图中只有一个大无障碍焦点,因为这是一个View,里面的文字及蓝色的矩形都是绘制出来的。


@Override

public void onDraw(Canvas c) {

    super.onDraw(c);

    if (mTitle != null) {

        drawTitle(c);

    }

    for (int i = 0; i < msize;="" i++)="">

        drawBarAtIndex(c, i);

    }

    drawAxisY(c);

}


代码中绘制出来的元素不可被TalkBack识别出来,所以开发需要多做一步,对自定义View无障碍化。这里将介绍如何通过官方提供的ExploreByTouchHelper来实现。


图3 自定义View内元素获得无障碍焦点


图3是使用ExploreByTouchHelper实现的最终效果,每一个小矩形都能获取到无障碍焦点,且可以进行选中高亮。实现ExploreByTouchHelper需要五步。

 
第一步,委托处理无障碍。


public class BarGraphView extends View {

    private final BarGraphAccessHelper mBarGraphAccessHelper;

    public BarGraphView(Context context, AttributeSet attrs, int defStyle) {

            super(context, attrs, defStyle);

            ...

            mBarGraphAccessHelper = new BarGraphAccessHelper(this);

            ViewCompat.setAccessibilityDelegate(this, mBarGraphAccessHelper);

    }


    @Override

    public boolean dispatchHoverEvent(MotionEvent event) {

        if ((mBarGraphAccessHelper != null)

                && mBarGraphAccessHelper.dispatchHoverEvent(event)) {

            return true;

        }

        return super.dispatchHoverEvent(event);

    }

}


mBarGraphAccessHelper继承ExploreBy TouchHelper,可通过注册AccessibilityDelegate的方式来注册给自定义BarGraphView,同时让mBarGraphAccessHelper来处理Hover事件(无障碍模式下的点击)的分发。


第二步,标记无障碍虚拟节点ID。


private class BarGraphAccessHelper extends ExploreByTouchHelper {

    private final Rect mTempParentBounds = new Rect();

    public BarGraphAccessHelper(View parentView) {

        super(parentView);

    }

    @Override

    protected int getVirtualViewIdAt(float x, float y) {

        final int index = getBarIndexAt(x, y);

        if (index >= 0) {

            return index;

        }

        return ExploreByTouchHelper.INVALID_ID;

    }

    @Override

    protected void getVisibleVirtualViewIds(List virtualViewIds) {

        final int count = getBarCount();

        for (int index = 0; index < count;="" index++)="">

        virtualViewIds.add(index);

        }

    }

}


getVirtualViewIdAt和getVisibleVirtualViewIds都是ExploreByTouchHelper类需要实现的方法,分别代表获取虚拟无障碍节点的id以及设置虚拟无障碍节点的id。由于自定义View里的元素非继承于View,如要在无障碍模式下被识别,则需要构造一个虚拟无障碍节点。构造方法已封装到ExploreByTouchHelper里,开发只需要告诉ExploreByTouchHelper有哪些虚拟无障碍节点的id即可。无障碍节点id需要满足以下条件:id是一个接一个的,稳定且为非负整数。设置好无障碍虚拟节点id后,根据用户操作UI上的xy坐标,取得对应的无障碍虚拟节点id,通过getVirtualViewIdAt方法告诉ExploreByTouchHelper类。


第三步,填充无障碍节点的属性。


private class BarGraphAccessHelper extends ExploreByTouchHelper {

    ...

    private CharSequence getDescriptionForIndex(int index) {

        final int value = getBarValue(index);

        final int templateRes = ((mHighlightedIndex == index) ?

                R.string.bar_desc_highlight : R.string.bar_desc);

        return getContext().getString(templateRes, index, value);

    }


    @Override

    protected void populateEventForVirtualViewId(int virtualViewId, AccessibilityEvent event) {

        final CharSequence desc = getDescriptionForIndex(virtualViewId);

        event.setContentDescription(desc);

    }


    @Override

    protected void populateNodeForVirtualViewId(

            int virtualViewId, AccessibilityNodeInfoCompat node) {

        final CharSequence desc = getDescriptionForIndex(virtualViewId);

        node.setContentDescription(desc);

        node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);

        final Rect bounds = getBoundsForIndex(virtualViewId, mTempParentBounds);

        node.setBoundsInParent(bounds);

    }

}


构造了虚拟无障碍节点后,便可以往节点里塞无障碍信息。populateEventForVirtualViewId是将无障碍信息填入无障碍事件中。populateNodeForVirtualViewId是初始化每个虚拟无障碍节点,设置contentDescription,注册所需要处理的Action,以及设置无障碍焦点的边框。setBoundsInParent一定要设置有效的边框,否则会导致虚拟无障碍节点无法获取无障碍焦点。


第四步,提供用户无障碍交互支持。


private class BarGraphAccessHelper extends ExploreByTouchHelper {

    ...

    @Override

    protected boolean performActionForVirtualViewId(

        int virtualViewId, int action, Bundle arguments) {

        switch (action) {

        case AccessibilityNodeInfoCompat.ACTION_CLICK:

            onBarClicked(virtualViewId);

            return true;

        }


        return false;

    }

}


private void onBarClicked(int index) {

    setSelection(index);

    if (mBarGraphAccessHelper != null) {

        mBarGraphAccessHelper.sendEventForVirtualViewId(

                index, AccessibilityEvent.TYPE_VIEW_CLICKED);

    }

}


小矩形点击后是会被选中且高亮的,在performActionForVirtualViewId中实现对应的点击事件处理。经过这四步,自定义View就可以完美支持无障碍化了!


可以看出,ExploreByTouchHelper简化了虚拟节点层次结构的构造,封装AccessibilityNodeProvider的实现,更完善的控制Hover事件、无障碍事件。有了它,Android无障碍化再也不是难题。


Android无障碍化宝典的内容就介绍到此,在实际开发中,遇到的无障碍化问题都比较细小和琐碎,希望以上介绍能提供一点帮助。很多Android开发以为无障碍化就是为控件加上ContentDescription,其实还有空描述、混乱焦点、焦点顺序、描述准确性等地方需要注意和优化。只有用心、持续地改进和优化,才能做出真正无障碍的产品。 


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多