Android-Accessibility(Android 8.

作者: Dufre | 来源:发表于2018-10-09 20:35 被阅读24次

    Accessibility Overview

    Accessible design allows users of all abilities to navigate, understand, and use your UI successfully.Android Accessibility的目的在于让所有的用户都能更方便的使用Android设备,不仅为残障人士提供了便利,更是方便了all users,比如你在开车,在做饭的时候。
    说个题外话,看Google I/O 2017或2018开发者大会视频,讲解Accessibility in Android的Product Manager(Patrick Clary)and Technical Program Manager(Victor Tsaran)是两位残障人士,由衷的敬佩。

    在这里插入图片描述在这里插入图片描述

    Impact of Accessibility

    • Increase your app's reach.
    • Improve your app's versatility.

    全世界约有15%的残障人士,如果能使你的APP more Accessibility,那么会有更多的人使用;另外Accessibility不仅仅方便了残障人士,也能方便所有人,比如你在开车的时候也能用语音代替手势或点击行为。也有可以做一些外挂,比如微信抢红包什么的。

    Build accessibility services

    在分析内部的AccessibilityService之前,我们先来看看构建accessibility service基础知识,先有个宏观上的认识,再去分析内部实现。

    Manifest declarations and permissions

    Accessibility service declaration

    Accessibility Service也是一个服务,需要在AndroidManifest.xml中声明,注意这里必须有BIND_ACCESSIBILITY_SERVICE permission,让Android System知道可以绑定。

    <application>
        <service android:name=".MyAccessibilityService"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
            android:label="@string/accessibility_service_label">
          <intent-filter>
            <action android:name="android.accessibilityservice.AccessibilityService" />
          </intent-filter>
        </service>
      </application>
    

    Accessibility service configuration

    Accessibility Service还需要配置信息,指定服务处理时的accessibility events类型和关于服务的附加信息。配置信息可以通过AccessibilityServiceInfo类的setServiceInfo()方法配置。Android4.0后,可以在Manifest中包含 < meta-data> 元素。

    For example:

    AndroidManifest.xml

    <service android:name=".MyAccessibilityService">
      ...
      <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/accessibility_service_config" />
    </service>
    

    < project_dir>/res/xml/accessibility_service_config.xml

    <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
        android:description="@string/accessibility_service_description"
        android:packageNames="com.example.android.apis"
        android:accessibilityEventTypes="typeAllMask"
        android:accessibilityFlags="flagDefault"
        android:accessibilityFeedbackType="feedbackSpoken"
        android:notificationTimeout="100"
        android:canRetrieveWindowContent="true"
        android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"
    />
    

    属性的解释


    在这里插入图片描述在这里插入图片描述

    Accessibility service methods

    自己写的Accessibility Service必须从AccessibilityService继承,然后再重写里面的方法,这些方法都是被Android System调用。

    • start service:onServiceConnected()
    • running service:onAccessibilityEvent(), onInterrupt()
    • close service:onUnbind()

    Register for accessibility events

    Accessibility service configuration一个很重要的功能是告诉指定Accessibility Service可以处理哪些Accessibility Event,这些event可以靠两种方式判别:

    • Package Names
    • Event Types

    Android Framework可能会把AccessibilityEvent分发给多个Accessibility Service,前提是这些Accessibility Service有不同的Feedback Types。如果多个Accessibility Service的Feedback Types相同,只有第一个注册的Accessibility Service会接收到这个Accessibility Event。

    Accessibility volume

    Android 8.0 (API level 26) 及以上,包含了STREAM_ACCESSIBILITYvolume category,可以独立于设备上其他的声音,只控制accessibility service的声音输出。通过调用AudioManager实例的 adjustStreamVolume()方法调节accessibility service的音量。

    For example:

    import static android.media.AudioManager.*;
    
    public class MyAccessibilityService extends AccessibilityService {
        private AudioManager mAudioManager =
                (AudioManager) getSystemService(AUDIO_SERVICE);
    
        @Override
        public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
            AccessibilityNodeInfo interactedNodeInfo =
                    accessibilityEvent.getSource();
            if (interactedNodeInfo.getText().equals("Increase volume")) {
                mAudioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY,
                    ADJUST_RAISE, 0);
            }
        }
    }
    

    Accessibility shortcut

    用户可以通过长按两个音量键来启用和禁用他们喜欢的Accessibility Service。

    Accessibility button

    导航栏的右侧包括一个Accessibility button。当用户按下这个按钮时,可以根据屏幕上显示的内容调用几个启用的Accessibility Service。Accessibility Service需要add FLAG_REQUEST_ACCESSIBILITY_BUTTONflag(android:accessibilityFlags),然后调用registerAccessibilityButtonCallback()

    For example:

    private AccessibilityButtonController mAccessibilityButtonController;
    private AccessibilityButtonController
            .AccessibilityButtonCallback mAccessibilityButtonCallback;
    private boolean mIsAccessibilityButtonAvailable;
    
    @Override
    protected void onServiceConnected() {
        mAccessibilityButtonController = getAccessibilityButtonController();
        mIsAccessibilityButtonAvailable =
                mAccessibilityButtonController.isAccessibilityButtonAvailable();
    
        if (!mIsAccessibilityButtonAvailable) {
            return;
        }
    
        AccessibilityServiceInfo serviceInfo = getServiceInfo();
        serviceInfo.flags
                |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
        setServiceInfo(serviceInfo);
    
        mAccessibilityButtonCallback =
            new AccessibilityButtonController.AccessibilityButtonCallback() {
                @Override
                public void onClicked(AccessibilityButtonController controller) {
                    Log.d("MY_APP_TAG", "Accessibility button pressed!");
    
                    // Add custom logic for a service to react to the
                    // accessibility button being pressed.
                }
    
                @Override
                public void onAvailabilityChanged(
                  AccessibilityButtonController controller, boolean available) {
                    if (controller.equals(mAccessibilityButtonController)) {
                        mIsAccessibilityButtonAvailable = available;
                    }
                }
            };
    
        if (mAccessibilityButtonCallback != null) {
            mAccessibilityButtonController.registerAccessibilityButtonCallback(
                    mAccessibilityButtonCallback, null);
        }
    }
    

    Fingerprint gestures

    Accessibility Service可以响应另一种输入机制,即在设备的指纹传感器上进行定向滑动(向上、向下、左、右)。配置一个Service来接收这些交互的回调,需要完成以下步骤:

    1. 声明USE_FINGERPRINTpermission 和 CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTUREScapability.
    2. 设置 FLAG_REQUEST_FINGERPRINT_GESTURESflag(android:accessibilityFlags).
    3. 注册函数registerFingerprintGestureCallback().

    For example:
    AndroidManifest.xml

    <manifest ... >
        <uses-permission android:name="android.permission.USE_FINGERPRINT" />
        ...
        <application>
            <service android:name="com.example.MyFingerprintGestureService" ... >
                <meta-data
                    android:name="android.accessibilityservice"
                    android:resource="@xml/myfingerprintgestureservice" />
            </service>
        </application>
    </manifest>
    

    myfingerprintgestureservice.xml

    <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
        ...
        android:accessibilityFlags=" ... |flagRequestFingerprintGestures"
        android:canRequestFingerprintGestures="true"
        ... />
    

    MyFingerprintGestureService.java

    import static android.accessibilityservice.FingerprintGestureController.*;
    
    public class MyFingerprintGestureService extends AccessibilityService {
        private FingerprintGestureController mGestureController;
        private FingerprintGestureController
                .FingerprintGestureCallback mFingerprintGestureCallback;
        private boolean mIsGestureDetectionAvailable;
    
        @Override
        public void onCreate() {
            mGestureController = getFingerprintGestureController();
            mIsGestureDetectionAvailable =
                    mGestureController.isGestureDetectionAvailable();
        }
    
        @Override
        protected void onServiceConnected() {
            if (mFingerprintGestureCallback != null
                    || !mIsGestureDetectionAvailable) {
                return;
            }
    
            mFingerprintGestureCallback =
                   new FingerprintGestureController.FingerprintGestureCallback() {
                @Override
                public void onGestureDetected(int gesture) {
                    switch (gesture) {
                        case FINGERPRINT_GESTURE_SWIPE_DOWN:
                            moveGameCursorDown();
                            break;
                        case FINGERPRINT_GESTURE_SWIPE_LEFT:
                            moveGameCursorLeft();
                            break;
                        case FINGERPRINT_GESTURE_SWIPE_RIGHT:
                            moveGameCursorRight();
                            break;
                        case FINGERPRINT_GESTURE_SWIPE_UP:
                            moveGameCursorUp();
                            break;
                        default:
                            Log.e(MY_APP_TAG,
                                      "Error: Unknown gesture type detected!");
                            break;
                    }
                }
    
                @Override
                public void onGestureDetectionAvailabilityChanged(boolean available) {
                    mIsGestureDetectionAvailable = available;
                }
            };
    
            if (mFingerprintGestureCallback != null) {
                mGestureController.registerFingerprintGestureCallback(
                        mFingerprintGestureCallback, null);
            }
        }
    }
    

    Multilingual text to speech

    text-to-speech (TTS) service可以在一个文本块识别和使用多种语言,要启用这种功能,需要将LocaleSpan对象中的所有字符串封装起来

    For example:

    TextView localeWrappedTextView = findViewById(R.id.my_french_greeting_text);
    localeWrappedTextView.setText(wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE));
    
    private SpannableStringBuilder wrapTextInLocaleSpan(
            CharSequence originalText, Locale loc) {
        SpannableStringBuilder myLocaleBuilder =
                new SpannableStringBuilder(originalText);
        myLocaleBuilder.setSpan(new LocaleSpan(loc), 0,
                originalText.length() - 1, 0);
        return myLocaleBuilder;
    }
    

    Take action for users

    从Android 4.0起,Accessibility Service可以代替用户做出Action,比如改变焦点(焦点就是当前正在处理事件的位置,比如有多个text输入框,同一时间内只能有一个输入框可以输入),模拟点击,模拟手势等等。

    Listen for gestures

    Accessibility Service可以监听特定的手势,然后代替用户做出反应。需要设置flags FLAG_REQUEST_TOUCH_EXPLORATION_MODE

    public class MyAccessibilityService extends AccessibilityService {
        @Override
        public void onCreate() {
            getServiceInfo().flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
        }
        ...
    }
    

    Continued gestures

    可以通过Path表示手势的路径,然后用GestureDescription.StrokeDescription构造手势。

    For example:

    // Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
    private void doRightThenDownDrag() {
        Path dragRightPath = new Path();
        dragRightPath.moveTo(200, 200);
        dragRightPath.lineTo(400, 200);
        long dragRightDuration = 500L; // 0.5 second
    
        // The starting point of the second path must match
        // the ending point of the first path.
        Path dragDownPath = new Path();
        dragDownPath.moveTo(400, 200);
        dragDownPath.lineTo(400, 400);
        long dragDownDuration = 500L;
        GestureDescription.StrokeDescription rightThenDownDrag =
                new GestureDescription.StrokeDescription(dragRightPath, 0L,
                dragRightDuration, true);
        rightThenDownDrag.continueStroke(dragDownPath, dragRightDuration,
                dragDownDuration, false);
    }
    

    Use accessibility actions

    可以通过getSource()得到node,然后调用performAction做出Action。

    public class MyAccessibilityService extends AccessibilityService {
    
        @Override
        public void onAccessibilityEvent(AccessibilityEvent event) {
            // get the source node of the event
            AccessibilityNodeInfo nodeInfo = event.getSource();
    
            // Use the event and node information to determine
            // what action to take
    
            // take action on behalf of the user
            nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
    
            // recycle the nodeInfo object
            nodeInfo.recycle();
        }
        ...
    }
    

    Use focus types

    可以用AccessibilityNodeInfo.findFocus()查找node有哪些元素具有Input Focus or Accessibility Focus,还可以使用focusSearch()选择Input Focus。最后使用performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS)设置Accessibility Focus。

    Gather information

    Accessibility services also have standard methods of gathering and representing key units of user-provided information, such as event details, text, and numbers.

    Get event details

    • AccessibilityEvent.getRecordCount()getRecord(int)
    • AccessibilityEvent.getSource()- 返回一个AccessibilityNodeInfo对象

    Process text

    Hint text

    • isShowingHintText()and setShowingHintText()
    • getHintText()

    Locations of on-screen text characters

    • refreshWithExtraData()

    Standardized one-sided range values

    一些AccessibilityNodeInfo对象用AccessibilityNodeInfo.RangeInfo的实例表示UI元素的范围值。

    • For ranges with no minimum, Float.NEGATIVE_INFINITYrepresents the minimum value.
    • For ranges with no maximum, Float.POSITIVE_INFINITYrepresents the maximum value.

    Build an accessibility service

    这一部分会介绍构建一个accessibility service的flow,从应用程序接收到的信息,然后该信息反馈给用户。

    Create your accessibility service

    create class

    package com.example.android.apis.accessibility;
    
    import android.accessibilityservice.AccessibilityService;
    import android.view.accessibility.AccessibilityEvent;
    
    public class MyAccessibilityService extends AccessibilityService {
    ...
        @Override
        public void onAccessibilityEvent(AccessibilityEvent event) {
        }
    
        @Override
        public void onInterrupt() {
        }
    
    ...
    }
    

    AndroidManifest.xml

    <application ...>
    ...
    <service android:name=".MyAccessibilityService">
         <intent-filter>
             <action android:name="android.accessibilityservice.AccessibilityService" />
         </intent-filter>
         . . .
    </service>
    ...
    </application>
    

    Configure your accessibility service

    告诉Android System,你想怎么运行,何时运行,你想对何种AccessibilityEvent做出回应,Service是否要监听所有Application,还是特定的Application,使用哪种feedback types。
    有两种方法,一种是setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo),然后重写onServiceConnected()
    For example:

    @Override
    public void onServiceConnected() {
        // Set the type of events that this service wants to listen to.  Others
        // won't be passed to this service.
        info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
                AccessibilityEvent.TYPE_VIEW_FOCUSED;
    
        // If you only want this service to work with specific applications, set their
        // package names here.  Otherwise, when the service is activated, it will listen
        // to events from all applications.
        info.packageNames = new String[]
                {"com.example.android.myFirstApp", "com.example.android.mySecondApp"};
    
        // Set the type of feedback your service will provide.
        info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
    
        // Default services are invoked only if no package-specific ones are present
        // for the type of AccessibilityEvent generated.  This service *is*
        // application-specific, so the flag isn't necessary.  If this was a
        // general-purpose service, it would be worth considering setting the
        // DEFAULT flag.
    
        // info.flags = AccessibilityServiceInfo.DEFAULT;
    
        info.notificationTimeout = 100;
    
        this.setServiceInfo(info);
    
    }
    

    另一种方法是使用xml方法

    For example:

    <accessibility-service
         android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
         android:packageNames="com.example.android.myFirstApp, com.example.android.mySecondApp"
         android:accessibilityFeedbackType="feedbackSpoken"
         android:notificationTimeout="100"
         android:settingsActivity="com.example.android.apis.accessibility.TestBackActivity"
         android:canRetrieveWindowContent="true"
    />
    

    同时在AndroidManifest.xml中添加< meta-data>,假设XML file 在res/xml/serviceconfig.xml

    <service android:name=".MyAccessibilityService">
         <intent-filter>
             <action android:name="android.accessibilityservice.AccessibilityService" />
         </intent-filter>
         <meta-data android:name="android.accessibilityservice"
         android:resource="@xml/serviceconfig" />
    </service>
    

    Respond to accessibility events

    当监听有AccessibilityEvent时,使用onAccessibilityEvent(AccessibilityEvent)方法做出回应。用getEventType()获取AccessibilityEvent Type,getContentDescription()提取与触发事件的视图有关的标签文本。
    For example:

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        final int eventType = event.getEventType();
        String eventText = null;
        switch(eventType) {
            case AccessibilityEvent.TYPE_VIEW_CLICKED:
                eventText = "Clicked: ";
                break;
            case AccessibilityEvent.TYPE_VIEW_FOCUSED:
                eventText = "Focused: ";
                break;
        }
    
        eventText = eventText + event.getContentDescription();
    
        // Do something nifty with this text, like speak the composed string
        // back to the user.
        speakToUser(eventText);
        ...
    }
    

    Query the view hierarchy for more context

    有时候,需要得到视图的相关信息,可以查看视图的层次关系,为了做到这一点,需要现在xml中配置。

    android:canRetrieveWindowContent="true"
    

    使用getSource()获得AccessibilityNodeInfo对象,当接收到一个AccessibilityEvent时,它会做以下事情:

    1.抓住该事件视图的父节点。
    2.在该视图(父节点)寻找label and check box作为子视图。
    3.如果它找到了它们,就创建一个字符串来向用户报告,指示标签,以及是否检查了它。
    4.如果在遍历视图层级时返回null(不管什么时候),那么该方法就会放弃。

    For example:

    // Alternative onAccessibilityEvent, that uses AccessibilityNodeInfo
    
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    
        AccessibilityNodeInfo source = event.getSource();
        if (source == null) {
            return;
        }
    
        // Grab the parent of the view that fired the event.
        AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
        if (rowNode == null) {
            return;
        }
    
        // Using this parent, get references to both child nodes, the label and the checkbox.
        AccessibilityNodeInfo labelNode = rowNode.getChild(0);
        if (labelNode == null) {
            rowNode.recycle();
            return;
        }
    
        AccessibilityNodeInfo completeNode = rowNode.getChild(1);
        if (completeNode == null) {
            rowNode.recycle();
            return;
        }
    
        // Determine what the task is and whether or not it's complete, based on
        // the text inside the label, and the state of the check-box.
        if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
            rowNode.recycle();
            return;
        }
    
        CharSequence taskLabel = labelNode.getText();
        final boolean isComplete = completeNode.isChecked();
        String completeStr = null;
    
        if (isComplete) {
            completeStr = getString(R.string.checked);
        } else {
            completeStr = getString(R.string.not_checked);
        }
        String reportStr = taskLabel + completeStr;
        speakToUser(reportStr);
    }
    

    以上基本是Android官网的学习资料,下一篇会学习AccessibilityService的源码,看看内部是怎么实现的。

    相关文章

      网友评论

        本文标题:Android-Accessibility(Android 8.

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