【文章內容使用 Gemini 1.5 Pro 自動產生】
Flutter 如何讓畫布渲染的應用程式對輔助技術使用者更友善
Flutter 框架支援的目標平台之一是 Web。Flutter 應用程式透過將所有 UI 渲染到畫布元素上,來保證像素精準度和平台一致性。但是,預設情況下,畫布元素不可存取。本案例研究說明了此類畫布渲染的 Flutter 應用程式如何支援存取性。
Flutter 擁有大量的預設 Widget,這些 Widget 會 自動產生存取性樹。存取性樹是一個存取性物件的樹,輔助技術可以查詢其屬性和特性,並在其上執行操作。對於自訂 Widget,Flutter 的 Semantics 類別允許開發人員描述 Widget 的含義,幫助輔助技術理解 Widget 的內容。
出於效能考量,在撰寫本文時,Flutter 的存取性預設情況下是選擇性啟用的。Flutter 團隊希望最終在 Flutter Web 中預設啟用語義。然而,在目前的狀況下,這會在許多情況下導致顯著的效能成本,並且需要一些優化才能更改預設值。希望始終啟用 Flutter 存取性模式的開發人員可以使用以下程式碼片段:
1 | import 'package:flutter/semantics.dart'; |
注意:如果您的應用程式絕對需要知道使用者是否正在使用螢幕閱讀器等輔助設備,請允許使用者選擇性啟用。
啟用 Flutter 的存取性支援後,HTML 會自動更改,如本頁面的其餘部分所示。
注意: 螢幕閱讀器只是利用所述方法獲益的輔助技術的一個例子。為了提高可讀性,螢幕閱讀器被用作此類技術和其他輔助技術的代表。
Flutter 的存取性選擇性啟用
Flutter 的選擇性啟用機制是一個隱藏的按鈕。它在 HTML 中放置一個按鈕,準確地說,是一個 flt-semantics-placeholder
元素,其 role 等於 “button” - 對有視力的人來說是不可見且不可達的。它是一個自訂元素,已應用樣式,因此它不會顯示,除非您使用螢幕閱讀器,否則不可選取。
1 | <flt-semantics-placeholder |
1 | /* `<flt-semantics-placeholder>` inherits from `<flutter-view>`. */ |
啟用後的變更
當一個使用螢幕閱讀器的使用者點擊這個按鈕時會發生什麼?考慮一個不太複雜的例子,例如 Flutter Gallery 中的 卡片,如以下螢幕截圖所示。
為了更好地理解使用者點擊按鈕時發生了什麼變化,請比較啟用和未啟用 Chrome DevTools 的螢幕截圖,當您 檢查存取性樹 時。第二張螢幕截圖顯示的語義資訊比第一張多很多。
未啟用:
已啟用:
實作細節
Flutter 的核心思路是建立一個可存取的 DOM 結構,反映目前在畫布上顯示的內容。這包括一個 flt-semantics-host
父自訂元素,它具有 flt-semantics
和 flt-semantics-container
子元素,而這些子元素又可以巢狀。考慮一個按鈕 Widget,例如 TextButton。此 Widget 在 DOM 中由 flt-semantics
元素表示。flt-semantics
元素上的 ARIA 標註(例如,role 或 aria-label)和其他 DOM 屬性(tabindex,事件監聽器)允許螢幕閱讀器向使用者宣佈這個元素為按鈕,並支援點擊和點選它,即使它不是一個字面上的 元素。在以下螢幕截圖中,分享 按鈕就是此類按鈕的一個例子。
此 flt-semantics
元素被絕對定位,以精準地出現在畫布上繪製對應按鈕的位置。這是因為 Flutter 擁有所有 Widget 的佈局,並且它預先計算每個語義節點的位置和大小。絕對佈局允許將存取性元素精確地放置在使用者預期的位置。但是,這也意味著使用者捲動時需要調整位置,在某些情況下會很昂貴。
將方法擴展到所有預設 Widget
由於 Flutter 知道在 Flutter 原始程式碼的 DOM 結構中被表示為 <flt-semantics role="button">
的內容原本是 Flutter 的 TextButton,因此將方法擴展並建立從所有現有的 Flutter Widget 到對應的 WAI-ARIA 角色 的映射相對容易,而這正是 Flutter 為所有預設 Widget 開箱即用的方式。例如,Flutter 目前支援以下角色:
- 文字
- 按鈕
- 複選框
- 單選框
- 文字欄位
- 連結
- 對話方塊
- 圖片
- 滑桿
- 即時區域
- 可捲軸
- 容器和群組
請注意,即使角色列表很短,許多不同的 Widget 類別也經常共享相同的角色。例如,Material TextField 和 CupertinoTextField 可以共享相同的文字欄位角色。大多數佈局 Widget,例如 Stack、Column、Row、Flex 等,都可以由容器/群組表示。
自訂 Widget 的挑戰
建立自訂 Widget 時,Flutter 可能無法自動為其應用正確的角色。如果 Widget 只是現有 Widget 的裝飾變體(例如,EditableText 的包裝器),它可能會正確呈現(作為文字欄位)。但是,如果您從頭開始建立 Widget,Flutter 預期您使用 Semantics Widget 來描述其存取性屬性。WAI-ARIA 定義了許多不同的 Widget 角色。Flutter 只支援部分角色,儘管這個子集正在不斷擴大。
例如,您可以探索 I/O 翻轉遊戲 中的團隊類別選擇器,如以下螢幕截圖所示。在 Web 術語中,它本質上是一個 <select>
,或者在 WAI-ARIA 術語中是一個 listbox。雖然可用選項被表示為 generic 文字(它們應該是 元素),但一個更大的問題是,從存取性樹中不清楚是否有更多選項可供選擇,但這些選項位於 Widget 的視窗範圍之外。請注意,在捲軸之前和之後,存取性樹中的可用選項。
捲軸之前:
捲軸之後:
如果您查看 原始程式碼,您會發現它沒有使用 Semantics 類別,因為 Semantics 尚未支援 listbox 和 option 角色標註的用例。但它確實使用了一個 ListWheelScrollView,它類似於一個普通的 ListView,因此它知道自己正在處理一個列表。但是,請注意,存取性樹只顯示目前可見的項目,以及視窗範圍上方和下方的一些項目,但從未顯示所有項目。(這是一個常見的應用程式效能技巧,我們幾乎也曾在 Web 上以 <virtual-scroller>
的形式原生獲得它。)
將 Flutter 的存取性樹與 ARIA 作者實務指南 中的 可捲軸 listbox 範例 進行比較,在該範例中,所有選項都顯示在存取性樹中,即使是那些位於視窗範圍之外的選項也是如此。在撰寫本文時,不完全支援此 listbox 用例是 Flutter 解決方案的一個缺點,將來會得到解決。
文字編輯
Flutter 有一個 flt-text-editing-host
元素,它有一個 <input>
或 <textarea>
作為其子元素,它將其像素精準地放置到對應的畫布區域。這意味著瀏覽器的便利功能(如自動填寫)按預期工作。此功能始終啟用,無論是否啟用存取性。在語義樹中,文字欄位由 <input>
元素表示,可能帶有一個描述它的 ARIA 標籤。以下 文字欄位 範例來自 Flutter Gallery。請查看使用者按下 Tab 鍵時 <input>
欄位是如何動態重新定位的。
雖然對於有視力的人來說,在文字輸入中顯示的標籤文字是可見的,但對於使用螢幕閱讀器的使用者來說,文字欄位會被宣佈為「編輯,空白」,在 Windows 上使用 NVDA 或在 macOS 上使用 VoiceOver,因為 Flutter 目前還沒有建立 <label>
元素。您可以在圖片底部看到 VoiceOver 的螢幕閱讀器輸出。這是 Flutter 將來會修復的一個問題。
當文字欄位被正確標籤化時,螢幕閱讀器會宣佈預期的含義,如以下純 HTML 範例所示。
結論
本案例研究深入探討了 Web 上 Flutter 畫布應用程式中存取性支援是如何運作的。Flutter 的存取性透過一個具有特定屬性和樣式的隱藏按鈕展開。啟用後,這種方法顯著改進了依賴螢幕閱讀器和其他輔助技術的使用者的體驗。Flutter 中的核心概念是建立一個可存取的 DOM 結構,鏡像畫布顯示,利用自訂元素,例如 flt-semantics-host
、flt-semantics
、flt-semantics-container
等。
雖然 Flutter 巧妙地將預設 Widget 映射到 WAI-ARIA 角色,但團隊承認還存在一些挑戰。對 Flutter 文字編輯的探索展示了使用 flt-text-editing-host
和 <input>
或 <textarea>
的技巧,證明了輸入欄位的動態重新定位。
展望未來,團隊已經開始研究如何進一步改進 Flutter 的存取性框架。這包括解決自訂 Widget 的 listbox 用例,以及增強文字編輯的標籤元素建立。這些預期的改進旨在提供更加全面和無縫的存取性體驗,反映 Flutter 對持續改進 Web 編譯目標的承諾。
致謝
本案例研究由 Flutter 團隊的 Yegor Jbanov、Kevin Moore、Michael Thomsen 和 Shams Zakhour 審查過其準確性。編輯審查由 Rachel Andrew 和 Shams Zakhour 提供。
Web 上的 Flutter 存取性 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。