藏在微信裏的溫度,無障礙開發框架分享

圖片

圖片

👉 騰小云導讀

現我國現有4471w視障/聽障人士,60歲及以上人羣達2.6億規模。微信作爲國民級應用,實現無障礙迫在眉睫。爲了幫助他們更好地使用微信 App,Android微信完成了適老化及無障礙改造。本文主要介紹Android 微信開發團隊根據適老化及無障礙需求,完成的一個協助業務側進行無障礙功能開發的框架。希望能給廣大開發愛好者帶來幫助和啓發!


👉 看目錄,點收藏

1 無障礙需求框架背景

1.1 無障礙需求

1.2 框架簡介

2 無障礙開發基礎知識

2.1 讀屏軟件識別View原理

2.2 讀屏軟件後的事件分發原理

3 框架實現的整體流程和執行原理

3.1 整體流程

3.2 執行原理

4 核心說明:全局熱區補足機制

4.1 背景說明

4.2 具體實現

4.3 額外說明

5 走查工具

6 總結

01、無障礙需求框架

目前,業界已經有共識性的無障礙開發守則。例如 Web Content Accessibility Guidelines (WCAG) 2.0,它是由互聯網的主要國際標準組織萬維網聯盟 (W3C) 的Web可訪問性倡議 (WAI) 發佈的一系列 Web 可訪問性指南的一部分。

此外,WAI-ARIA(可訪問的富Internet應用程序套件)是由萬維網聯盟(W3C)發佈的一項關於 A11 Y技術應用規範。該規範定義了一種使殘障人士更易於訪問 Web 內容和 Web 應用程序的方法,增加 HTML、JavaScript 和相關技術開發的網站動態內容以及用戶界面組件的可訪問性。

目前,Android沒有官方統一、方便的框架,官方提供的原生api並不是特別好用,所以微信團隊對其進行參考,開發了一個無障礙框架,基於原生的api進行了再封裝,將繁瑣的無障礙適配邏輯封裝在底層,以聲明式接口的形式,讓上層業務能以更簡便更解耦的代碼,完成無障礙的適配。接下來我們進行分享:

1.1無障礙需求

本框架主要具備以下特性:

  • 可感知性 :包括大字體適配,顏色對比度等 。
  • 可操作性 :主要是過小熱區的放大,提高老年人/殘疾人的交互體驗 。
  • 可理解性 :微信應提供讀屏文案等信息,幫助盲人在開啓 Talkback 等讀屏軟件的情況下,正常使用微信。

下面給出一些較爲典型的需求:

  • 需求1:過小熱區的放大

需求是要求微信內的所有可交互控件,可點擊範圍不得低於 44dp * 44dp。

大小不合規的控件,如果一個個進行排查、佈局修改。工程量龐大。

  • 需求2:響應區域會隨無障礙開關發生變化

圖片

該 Item 由一個 SwitchButton + TextView 組成。

開啓 Talkback 時,整個 Item 識別爲一個焦點,選中雙擊是觸發點擊 switch的邏輯。在無障礙模式下,選中雙擊是直接觸發相應控件的 Click 事件。但是在不開 Talkback 的情況下點擊 Item 又無需響應,只響應 SwitchButton 。也就是點擊區域會隨 Talkback 開關發生變化。

實現可能是:在 ItemClick 中進行 if 判斷。但這樣寫侵入性高,難維護。

  • 需求3:讀屏文案由其他的控件的值組合

圖片

選中頭像,讀屏文案:騰訊行政的頭像,有 2 條未讀消息。需要讀出列表中其他關聯內容,這種只能把適配代碼侵入到 Adapter中。

1.2 框架簡介

框架將不同的無障礙需求的實現進行封裝,抽象成不同的規則。

業務側可以將一個頁面/業務的無障礙需求,在一個配置類裏使用規則表達出來,再由框架進行處理。實現相應的效果。

class ChatAccessibility(activity: AppCompatActivity) :  
BaseAccessibilityConfig(activity) {  
  override fun initConfig() {  
        // 設置 contentDesc  
   view(rootId,viewId).desc(R.string.send_smiley)  
        // ...  
  }  
}

框架基類 BaseAccessibilityConfig 提供了一系列用於表達規則的 api,包括但不限於如下功能:

  • 通過配置統一設置 contentDescription

  • 支持把多個 View 組合成一體進行讀屏

  • 通過配置禁用某個View被 Talkback 聚焦的能力

  • 支持按指定順序進行讀屏,支持局部控制 Talkback 聚焦順序

  • 支持設定在 Activity 啓動後的第一個讀屏控件

  • 支持對某個父 View 的 disableChildren 功能

  • 在某個 View 滿足條件時,對其進行讀屏,但不聚焦

  • 在某個 View 滿足條件時,讀出提前設定的 string,但不聚焦

  • 全局熱區寬高補齊至 44dp,並提供自定義熱區放大/禁用熱區放大的功能 ...

02、無障礙開發基礎知識

在深入瞭解框架的設計前,先來介紹一些無障礙功能開發的基礎知識。

2.1 基礎知識1:讀屏軟件識別 View 原理

圖片

讀屏軟件無法直接識別到View,只能識別到View提供的虛擬節點「Node」,View 和虛擬節點一般是一一對應的。當頁面內容發生變化,比如 View 被設值,或者發生滾動等情況,View 會向無障礙系統發送一個事件,通知系統。

然後系統就回頭向 View 索取節點,組成頁面更新後新的節點樹,而 「節點樹 和 ViewTree 是一一對應的」。此時讀屏軟件拿到的就是新的內容了。

2.2 基礎知識2:讀屏軟件後的事件分發流程

分爲上下兩部分:讀屏軟件攔截處理行爲、讀屏軟件接受事件。

圖片

流程如下:

  • 讀屏軟件攔截用戶 Touch 事件,根據事件的座標去定位到目標節點。
  • 將 Touch 事件解釋爲節點行爲,這裏以觸摸選中爲例,那麼就是聚焦行爲。
  • 讀屏軟件通過該節點向無障礙系統發送,無障礙系統又轉發給View(聚焦產生的綠框就是在View的內部處理裏去繪製的)。
  • 生成新的虛擬節點並提供給讀屏軟件後,讀屏軟件組合信息,通過 TTS 語音引擎的 api 讀出。

讀屏軟件展示給用戶的所有信息,全部來自虛擬節點。可以在節點生成的過程中,修改節點的信息,所以這裏是一個絕佳的**「信息自定義」**的地方。

採用將所有的 View 都 「Wrap 一層 AccessibilityDelegate」 的方式,「在 onInitializeAccessibilityNodeInfo 方法中修改節點信息」。

03、框架實現整體流程與執行原理

3.1 整體流程

圖片

  1. 業務側實現規則配置類,編寫的規則會進入配置池。

  2. 框架在View生成節點給系統的時候進行攔截 「(onInitializeAccessibilityNodeInfo)」

  3. 在配置池中尋找匹配的規則。

  4. 根據匹配的規則對節點進行修改。

  5. 最後生成的節點就會由系統交由給讀屏軟件進行讀屏。

3.2 執行原理

圖片

核心原理:採用基於責任鏈的流水線來處理。整體流程主要分爲兩部分:

  • View 預處理責任鏈(圖示左邊):執行預出來操作,如異步生成緩存、View標記等;

  • 節點處理責任鏈(圖示右邊):節點處理的同時會同步查找規則進行設置。

接下來主要簡單介紹下框架的一個核心功能的實現:「全局熱區補足機制」 (位於框架流程中的預處理責任鏈中的一環)。

04、核心說明:全局熱區補足機制

4.1 背景說明

  • 需求說明

過小熱區放大,即微信內的所有可交互控件可點擊範圍不得低於 44dp * 44dp,像一些大小不合規的控件,如果一個個進行排查、佈局修改,工程量太龐大。還有熱區其他一些需求 etc。

  • 問題難點

一般會選擇直接修改 padding,有些甚至需要改動相應佈局,但這樣的改動工作量太大且容易影響原來視圖佈局。

  • 解決方案

需要一個全局的熱區補足機制,將過小熱區補足至規範。

4.2 具體實現

「創建 View 的統一入口」 去設置 TouchDelegate 代理,由父 View 作爲TouchDelegate 的承載 View 去代理 Touch 事件,這裏有幾個問題需要解決:

  • 如何找到合適的承載View
  • 熱區及時更新
  • 性能優化
  • 讀屏模式下的熱區擴大

下面我們分別展開講。

  • 重點問題1:如何找到合適的承載 View

從目標 View 向上冒泡,找到一個合適的父 View。那麼需要 「冒泡終止條件」。 首先條件一肯定是 「足夠大」。當前 View 夠大了就沒必要再往上冒了。

圖片

但是這樣會存在問題:子 View 的 Click優先級高於父View的TouchDelegate。事件派發機制:

從父 View 往子 View 派發,從子 View 向上處理。View 的事件處理順序是先 OnTouchListener,然後是 TouchDelegate,再是Click、LongClick。

所以會導致下圖的情況:

圖片

目前進行了折中處理,相比上圖,顯然是下圖的放大後的體驗更佳:

圖片

同時加入了條件二:「該承載 View 是 Clickable、LongClickable」。最終方案流程確定如下:

圖片

  • 重點問題2:熱區及時更新

背景: 承載 View 的 TouchDelegate 需要的參數包含一個 Rect,也就是對擴大的熱區進行響應。

問題: 這個矩陣是提前傳入,且和 小 View 沒有直接的關係。如果小 View 的佈局發生變動,會導致擴大後熱區沒有及時跟上變化。導致熱區錯位。

解決方案: 在 小 View 的 onLayoutChange 中重新進行一遍 ·View 擴大方案· 的處理。同時爲了防止 onLayoutChange  執行過於頻繁,將 onLayoutChange 包裝成 View 的一個事件。如果短時間內多次 onLayoutChange  ,則只在最後一次 onLayoutChange 的時候進行  「View擴大方案」處理。

  • 重點問題3:性能優化

背景 :最初的 View 擴大方案執行時機是在創建 View 的統一入口,也就是在 LayoutInflate 的 onCreateView 中同步執行,每個 View 都得執行。

問題:由於 View 數量較爲龐大,所以存在較大的性能隱患。

解決方案:採用了異步方案並同時對 View 處理任務進行收攏。將執行時機提前到 LayoutInflate.inflate 並異步處理,在異步任務中去遍歷該 inflate 的根 View的所有子 View。儘量不去阻塞主線程的運行。

  • 重點問題4:讀屏模式下的熱區擴大

通過上面的實現,點擊熱區確實是擴大了。但是在讀屏模式下選中的時候,選中的框並沒有擴大。那麼首先需要知道,選中時的框是以什麼作爲 Bound。

綠框的繪製核心邏輯位於 ViewRootImpl 中的一個 drawAccessibilityFocusedDrawableIfNeeded(),該方法的調用時機是用戶觸摸選中某個View後,傳遞到 ViewRootImpl 時進行調用,也就是讀屏選中的綠框是由系統繪製的,而不是由讀屏軟件繪製的。從源碼中能夠得知的是,綠框的Bound 根據是否有虛擬節點,分爲兩種情況:

private void drawAccessibilityFocusedDrawableIfNeeded(Canvas canvas) {  
    final Rect bounds = mAttachInfo.mTmpInvalRect;  
    if (getAccessibilityFocusedRect(bounds)) {  
        final Drawable drawable = getAccessibilityFocusedDrawable();  
        if (drawable != null) {  
            drawable.setBounds(bounds);  
            drawable.draw(canvas);  
        }  
    } else if (mAttachInfo.mAccessibilityFocusDrawable != null) {  
        mAttachInfo.mAccessibilityFocusDrawable.setBounds(0, 0, 0, 0);  
    }  
}  
  
private boolean getAccessibilityFocusedRect(Rect bounds) {  
    ...  
    final AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider();  
    if (provider == null) {  
        host.getBoundsOnScreen(bounds, true);  
    } else if (mAccessibilityFocusedVirtualView != null) {  
        mAccessibilityFocusedVirtualView.getBoundsInScreen(bounds);  
    } else {  
        return false;  
    }  
  ...  
    return !bounds.isEmpty();  
}

經過跟蹤源碼發現,這是因爲 「綠框的繪製」 是根據 View.getBoundInScreen 獲取的矩陣來做到的。而 TouchDelegate 的設置無法改變 View.getBoundInScreen 獲取到的矩陣。在使用虛擬節點的情況下,纔會使用虛擬節點的Bound進行繪製。

對於這個問題,我們的解決思路是:

  • 對每個 View 設置自定義的 AccessibilityDelegate, 並實現其中的 getAccessibilityNodeProvider 方法。
  • 如果判斷 View 需要擴大,在 getAccessibilityNodeProvider 中返回自定義的 Provider。
  • 在自定義的 Provider 中,計算 View 的擴大後的矩陣在屏幕上的位置。
  • 將矩陣設置給虛擬節點,並返回給系統。

4.3 額外說明

  • 如何匹配規則與View?

框架將配置池按 Activity 劃分,極大減少衝突概率,同時減少配置池大小,加快查找規則的速度,提供 layoutId + viewId,rootId + viewId 兩種形式的 View 定位機制。由兩個 Id 確定一個 View,減少衝突。

  • 查找規則時間長可能導致的主線程卡頓?

由於查找規則的時機是在生成節點,是由系統觸發且無法異步。在查找規則的過程中,使用預處理的時候提前生成的緩存進行查找,儘可能減少耗時。

05、走查工具

5.1 背景

當完成無障礙需求的開發後,需進行驗證。在驗證過程中發現開啓驗證效率低下,需開啓讀屏軟件後,逐個元素驗證。

5.1.1 解決方案與原理

基於無障礙服務(AccessibilityService)開發、集成了在不開啓 Talkback 的情況下能展現讀屏區域一個無障礙功能走查工具,無需開啓 Talkback 逐個手動觸摸,就能高效檢查無障礙適配情況。

圖片

實現原理如下:

  • 自定義實現一個 AccessibilityService 用於獲取到當前活躍窗口的根節點。

  • 每隔 0.5s 進行一次節點的獲取:從當前活躍窗口的根節點遍歷所有的節點,逐個進行判斷是否會被聚焦。

  • 對通過允許聚焦的節點進行信息收集,在一次遍歷完成後通知到 DrawService。

  • 提前在window中添加一個 View 用於繪製信息,由 DrawService 進行繪製。

5.2 具體實現

關鍵實現:如何判斷一個節點能否被聚焦,即需理解 Talkback 是如何聚焦,流程如下:

1、如果是支持 WebView 中 Html 無障礙,特殊判斷。

2、如果不可見,則不聚焦。

3、判斷是否是畫中畫,像下圖的紅框這種就是畫中畫,如果是畫中畫,這個就是焦點。

圖片

4、該節點是否和 window 邊界重合等大。對於這種和 window 等大的節點,Talkback 選擇不做聚焦。

5、檢查該節點是否 clickable/longClickable/focusable 或者是列表的“會說話的” 頂層視圖(滿足->6 不滿足->7)列表(ListView/RecycleView)的頂層視圖例子如下:

圖片

但是聚焦的前提是“會說話的”。“會說話的”包括以下幾個條件:

  • HasText:包括 contentDescription、text、hintText(包括 Button 的 Text)。
  • hasStateDescription:包括 CheckBox 的已選未選狀態、進度條的進度狀態等。
  • hasNonActionableSpeakingChildren:含有無法聚焦、點擊但是 HasText 的子 View(如上圖通訊錄中的 “新的朋友” TextView,就是無法聚焦、點擊但是 HasText 的子 View)。

6、基本上滿足了步驟5就可以視爲可聚焦了,但是有一些View僅僅是 Focusable,但是卻 ”什麼話都沒得說“ ,對於這種 View 應該是要排除的。故按如下步驟做判斷:只要是沒有子節點的 focusable/clickable/longclickable 的 View,全部聚焦 、“會說話的” 全部聚焦 6.3 剩下的就不聚焦了(“不會說話”、“有子節點”)。

7、能到這一步,說明步驟 5 不滿足,即該節點是普通的不可聚焦的 View。但是防止錯過一些沒有點擊事件的 TextView 之類的需要聚焦,需要再最後做一步判斷(這一步也是啥爲了保證所有的信息都可以不遺漏);如果沒有可聚焦父節點,但仍然 hasText 或 hasStateDescription,聚集該節點。

8、一路闖關到這的 View,就終於逃離 TalkBack 的聚焦了。

06、總結

爲了幫助老年人、視障/聽障人羣等更好地使用微信 App,Android微信完成了適老化及無障礙改造如上。本文主要介紹 Android 微信開發團隊根據適老化及無障礙需求,完成的一個協助業務側進行無障礙功能開發的框架。我們在介紹了無障礙開發所涉及的2大重點基礎知識(讀屏識別View原理和讀屏軟件後的事件分發原理)之後,爲各位展開回顧了我們框架具體細節和方法。

以上是本次分享全部內容,歡迎大家在評論區分享交流。如果覺得內容有用,歡迎轉發~

-End-

原創作者|許懷鑫

技術責編|許懷鑫

圖片

現我國現有4471w視障/聽障人士,60歲及以上人羣達到2.6億規模。信息無障礙(Web Accessibility)的概念在近幾年受到關注。 信息無障礙是指通過信息化手段彌補身體機能、所處環境等存在的差異,使任何人(無論是健全人還是殘疾人、無論是年輕人還是老年人)都能平等、方便、安全地獲取、交互、使用信息。微信、QQ、騰訊新聞和騰訊地圖等應用加適老化元素,配備爲老人而設的“關懷模式”;搜狗輸入法推出爲視障羣體量身打造的“保益盲人輸入法”......

當說到無障礙,大家第一反應是弱勢羣體。實際上,無障礙是適用於全民的。每個人都可能有遇障時刻。當你手提重物或受傷時,你可能會選擇乘坐無障礙電梯;當你處在嘈雜的環境下看視頻時,你可能需要通過字幕獲取信息……每個人都是無障礙環境的受益者,視障、聽障人羣、含殘疾人、老年人是信息無障礙的重點受益羣體。

事件分享:你還見到過哪些讓你眼前一亮的信息無障礙案例?

腦洞時刻:程序員還可以爲信息無障礙做些什麼?

歡迎在公衆號評論區聊一聊你的看法。在4月10日前將你的評論記錄截圖,發送給騰訊雲開發者公衆號後臺,可領取騰訊雲「開發者春季限定紅包封面」一個,數量有限先到先得😄。我們還將選取點贊量最高的1位朋友,送出騰訊QQ公仔1個。4月10日中午12點開獎。快邀請你的開發者朋友們一起來參與吧!

回覆「微信」,領取更多微信的技術case和論文資源

圖片

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章