Android無障礙淺析

前言

Android無障礙,我們平常接觸時,比較熟悉的有“綠色守護”以及“搶紅包”這些,其便利性便是在沒有“root權(quán)限”的情況下,可以“觸摸”其他應(yīng)用來做一些操作。然而,無障礙的初衷卻是為視覺障礙的人提供操控手機的可能。

Android中的實現(xiàn)

Android包含幾個支持視覺障礙用戶訪問的特性;他們不需要應(yīng)用做出巨大的視覺改變。
TalkBack是由Google公司提供的一個預(yù)安裝屏幕閱讀服務(wù)。它使用語音反饋描述操作的結(jié)果(如啟動一個app)和事件(如通知)。
Explore by Touch(觸摸瀏覽)是與TalkBack協(xié)作的系統(tǒng)特性,允許用戶觸摸設(shè)備屏幕并通過語音反饋聽取手指觸摸的內(nèi)容。該特性對低視力用戶有幫助。
無障礙設(shè)置允許用戶修改設(shè)備的展示和聲音選擇,例如放大文本字體,改變文本閱讀的速度等等。
一些用戶使用硬件或軟件定向控制(例如,D-pad,軌跡球,鍵盤)從屏幕上的一個選擇跳轉(zhuǎn)到另一個選擇。他們以線性順序與應(yīng)用的結(jié)構(gòu)進行交互,這種線性順序類似于電視的四個方向遠程控制導(dǎo)航。

基本組件的無障礙開發(fā)

對于Android的基礎(chǔ)組件,只需要簡單的在xml或代碼中設(shè)置contentDescription屬性。

  • AccessibilityEvent : 在用戶和ui交互時,由系統(tǒng)發(fā)送的無障礙事件,例如按鈕被點擊,或者一個view被focus,參見AccessibilityService,一個無障礙事件的最主要作用就是暴露給AccessibilityService足夠多的信息,以提供給用戶界面良好的反饋。

  • AccessibilityNodeInfo:一個view狀態(tài)的快照,代表了窗口中包含的節(jié)點的信息。

  • View.AccessibilityDelegate:View 的內(nèi)部類,通過組合而非繼承的方式來控制處理無障礙事件。包括發(fā)送,初始化事件以及節(jié)點屬性。

辣么我們來看下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;
        }
    }

在初始化調(diào)用onInitializeAccessibilityEvent時,會將設(shè)置到view中的mContentDescription等屬性放進AccessibilityEvent中去。
無障礙事件如何發(fā)送呢?

 /**
     * 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中調(diào)用view 的sendAccessibilityEvent,由view中的內(nèi)部類對象AccessibilityDelegate來處理。
這樣就完成了一個完整的處理流程,從初始化->用戶接觸產(chǎn)生并發(fā)送事件->接受事件->talkback和 Explore by Touch 反饋給用戶。
這里有個問題,我們沒有設(shè)置過TextView中的contentDesciption屬性,為什么開啟無障礙后,居然能夠讀出上面的文字呢?

自定義view的無障礙開發(fā)

在做自定義view的開發(fā)時,會遇到一個問題,我們知道繼承View時,此時一個單獨的contentDescription是不能夠描述當(dāng)前的布局屬性的,來給無障礙很好的反饋支持,最典型的栗子便是月歷這個自定義view,單獨設(shè)置contentDescription時,我們只能知道它是一個月歷顯示,不能知道里面的每一個節(jié)點的具體信息,星期幾?幾號?
Android 便提供了一種解決方案:** 既然不是真實存在的,就虛擬出節(jié)點來。**

  • 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.

進一步的,為了降低開發(fā)成本,google為開發(fā)者提供了ExploreByTouchHelper來降低開發(fā)成本。
整個過程大致分三個步驟:
1 . 初始化

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

2 . 處理以及發(fā)送事件

    @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 . 生成虛擬節(jié)點以及描述信息初始化

 class MyExploreByTouchHelper extends ExploreByTouchHelper{
        public MyExploreByTouchHelper(View forView) {
            super(forView);
        }

        @Override
        protected int getVirtualViewAt(float x, float y) {
            //根據(jù)想x,y坐標(biāo)點返回虛擬節(jié)點的id
            return 0;
        }

        @Override
        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
            //虛擬節(jié)點的id list
        }

        @Override
        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent accessibilityEvent) {
            //填充無障礙事件的屬性,例如contentDescription
            accessibilityEvent.setContentDescription(getItemDescription(virtualViewId));

        }

        @Override
        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat accessibilityNodeInfoCompat) {
            //初始化虛擬節(jié)點的位置等其他屬性
            accessibilityNodeInfoCompat.setBoundsInParent(mTempRect);
            accessibilityNodeInfoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);

        }

        @Override
        protected boolean onPerformActionForVirtualView(int i, int i1, Bundle bundle) {
            return false;
        }
    }

查看

虛擬的節(jié)點如何能 查看到呢?

Android Studio ->Tools -> Android ->Android Device Monitor -> touch Process (選中某個界面進程) -> Dump View Hierarhchy for UI Automator。

這里dump 出來的不止真實的ui組件,還包括我們自建的虛擬的veiw樹,這樣更具象化,可以很好的定位問題。

總結(jié)

我們在做日常開發(fā)的過程中,很少會關(guān)注到無障礙這部分的內(nèi)容,但是仍有一部分人也希望能夠和我們一樣方便的使用移動設(shè)備,所以也希望開發(fā)者們能夠關(guān)注到這一點,雖然很小的一點改變,卻能讓這個世界更美好一點。
也有部分開發(fā)者通過創(chuàng)新,希望能夠利用到contentDescription字段做一些協(xié)議層面的東西,畢竟Android框架提供了這個不需要root權(quán)限變可以通過一個app來做跨應(yīng)用服務(wù)。但是這里可能也需要權(quán)衡,畢竟有一部分人是需要【初衷】的無障礙。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容