前面 29 天,從 GPS 記錄、背景定位,到推薦路線、離線同步、Widget,再到最後的 AI 分析,一開始預計要做的功能全部做完了。
最後一天想分享一個完全不一樣的內容:Accessibility,簡稱 a11y,中文通常稱為「無障礙」或「可訪問性」。
如果使用者看不到畫面上的資訊,這些功能還能不能使用?

我在很久之前看過一個影片,是採訪幾位視障程式設計師,看看他們平常是怎麼 coding 的:我采访了几位盲人程序员,看看他们是怎么写代码的【差评君】



影片裡有提到,視障者很依賴螢幕閱讀器(例如 iOS 的 VoiceOver、Android 的 TalkBack)。
如果 App 沒有為元件提供適當的說明,即使螢幕閱讀器可以讀到這個元件,也可能不知道它到底是做什麼的。
這也讓我開始思考:
如果今天 App 是由一個看不到畫面的使用者操作,現在做好的這些功能還能不能正常使用?
目前常見的無障礙標準是 WCAG(Web Content Accessibility Guidelines),主要由 W3C 制定。
WCAG 的核心可以簡單分成四個原則:

這些原則不只適用於網站,Mobile App 也可以參考。
不同平台都有自己的工具:
Android 可以使用 Accessibility Scanner,掃描 App 後會列出一些可能需要改善的地方。
macOS 本身就有 Accessibility Inspector,可以檢查 iOS App 的 Accessibility 資訊。

Chrome、Firefox、Safari、Edge 的開發者工具都有 Accessibility 相關功能。
自己使用下來,我覺得 Firefox 的 Accessibility 工具做得滿完整,除了檢查元素的 Accessibility Tree,也可以查看 Tab 順序、模擬色盲等。


Chrome 可以在 Elements 裡查看元素的 Accessibility 資訊,也可以使用 Lighthouse 做 Accessibility 評估。



另外 Chrome 的 Color Picker 也會直接顯示文字對比度。

Safari 的 Elements 裡也可以查看 Accessibility 屬性,另外還有專門的 Accessibility 審查工具。


也可以搭配瀏覽器擴充功能進行檢查。


文字和背景的對比度太低,會讓視力不好或有色覺障礙的使用者更難閱讀。
例如 WebAIM 的 Contrast Checker 可以直接檢查對比度,並告訴你是否符合 WCAG AA / AAA。

來源:文字背景对比度 contrast ratio 的计算公式
對比度計算工具:https://webaim.org/resources/contrastchecker/

如果想自己理解計算方式,可以參考相對亮度與對比度公式:
L = 0.2126 * R / 255
+ 0.7152 * G / 255
+ 0.0722 * B / 255
對比度 = (L1 + 0.05) / (L2 + 0.05)
另外也有一些配色網站可以直接參考:
按鈕太小也會影響操作。
一般可以參考:
React Native Pressable 可以使用 hitSlop 增加實際可點擊範圍。
<Pressable
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
onPress={() => console.log('onPress')}
>
<Text>Pressable</Text>
</Pressable>
例如 Icon 本身只有 24px,也可以透過 hitSlop 讓實際點擊範圍更大。
對 React Native 來說,最常接觸的幾個 Accessibility props 是:
accessible
accessibilityLabel
accessibilityHint
accessibilityRole
accessibilityState
accessibilityValue
例如:
<View
accessibilityLabel="view1"
accessibilityHint="我是 view1"
>
// ...
</View>
使用螢幕閱讀器時,就能知道這個元件叫做 view1,以及它的用途。
這些 API 在 Android 和 iOS 上會有一些差異,實際開發時還是需要搭配官方文件確認。
前面了解一些基本概念之後,就可以回到今天真正要做的事情。
這次沒有把所有畫面重新檢查一遍,而是直接拿 App 最重要的散步流程,用 旁白(VoiceOver) 和 螢幕閱讀器(TalkBack) 實際操作:
開始散步
↓
取得 GPS
↓
開始記錄
↓
暫停 / 繼續
↓
導航
↓
偏離路線
↓
完成散步
↓
結束
結果確實找到一些平常用眼睛操作時很難發現的問題。
散步畫面裡的結束按鈕原本只有一個 ■。
<Pressable onPress={handleFinish}>
<Text>■</Text>
</Pressable>
正常看畫面的人可以理解這代表結束,但螢幕閱讀器可能只會把它當成一個符號。
所以補上明確的 Accessibility 資訊:
<Pressable
accessibilityRole="button"
accessibilityLabel="結束散步"
onPress={handleFinish}
>
<Text accessible={false}>■</Text>
</Pressable>
■ 是給眼睛看的圖示,真正需要讓螢幕閱讀器知道的是「這個按鈕可以結束散步」。
暫停、繼續,以及定位還沒準備好的狀態,也一起確認了 Accessibility label 和 state。
導航畫面原本大概是:
↗
前方右轉
28 公尺
箭頭對視覺使用者來說很直覺,但螢幕閱讀器可能會把箭頭也讀出來。
但對使用者真正重要的是:下一步要做什麼,以及還有多遠。
所以加上 accessible={false} 把箭頭設為
<Text accessible={false}>↗</Text>
並把導航資訊整理成完整的 Accessibility label:下一步,前方右轉,剩餘 28 公尺
如果是其他狀態,就直接描述狀態 已偏離推薦路線,請返回推薦路線、已抵達終點
另外把這些文案整理到 navigationAccessibility.ts,讓 UI 和 Accessibility 文案分開。
這次還發現一個很有趣的問題。
導航距離會一直變:
28 公尺
26 公尺
24 公尺
22 公尺
20 公尺
如果每次距離變化都觸發螢幕閱讀器公告,使用者可能會一直被唸新的距離。
所以 Accessibility 並不是「資訊越多越好」。
這次把比較重要的狀態變化才設為需要主動通知,例如:
導航步驟改變
→ 通知
偏離路線
→ 通知
回到路線
→ 通知
抵達終點
→ 通知
只有距離變化
→ 不主動通知
實際上會根據 status、currentStepIndex、maneuver 等狀態判斷是不是值得通知。
這也是我覺得很容易忽略的一點:
不是所有資訊都需要被讀出來,真正重要的是讓使用者在正確的時間知道正確的事情。
推薦路線裡有一些可以選擇的項目,例如散步時間:
30 分鐘 45 分鐘 60 分鐘
假設目前選的是 45 分鐘,畫面上可能會用不同的背景色表示:
30 分鐘 [45 分鐘] 60 分鐘
對一般使用者來說,看顏色就知道 45 分鐘被選中了。
但 VoiceOver / TalkBack 不會知道「這個按鈕是不同顏色」,所以如果沒有額外設定,它可能只會知道這是一個「45 分鐘」的按鈕,卻不知道目前是不是選中的。
因此需要加上 accessibilityState:
<Pressable
accessibilityRole="button"
accessibilityState={{
selected: isSelected,
}}
>
<Text>45 分鐘</Text>
</Pressable>
這樣螢幕閱讀器就能知道目前這個選項是選中的。
提醒設定裡有類似的選項,也使用相同方式處理。
推薦路線裡有開始位置、時間、距離等輸入欄位。
例如:
開始位置
[ 輸入地址或地點名稱 ]
Placeholder 可以告訴使用者「要輸入什麼」,但不一定能清楚表達這個欄位本身的名稱。
所以還需要加上 accessibilityLabel:
<TextInput
accessibilityLabel="開始位置"
/>
其他輸入欄位同樣依照用途補上 Accessibility label。
Accessibility 可以做的事情非常多,所以我也不是看到什麼就全部重做。
今天已經是鐵人賽最後一天,所以先把重點放在核心散步流程:
開始散步
↓
暫停 / 繼續
↓
導航
↓
偏離路線
↓
結束散步
至少確保這些最重要的操作,都能被螢幕閱讀器正確理解。
回頭看這 30 天,TraceWalk 一開始其實只是一個很單純的想法:做一個散步 App。
但真的開始做之後,才發現「記錄一次散步」只是最基本的部分。
從 GPS 不準、背景定位,到路線推薦、Route Matching、導航、離線同步、Widget、統計,再到最後的 AI 分析,實際做下去才發現一個 App 還有很多原本沒有想到的問題。
這 30 天當然不可能把一個 App 做到真正完整,但至少 TraceWalk 已經從一開始的一個想法,變成了一個可以實際使用的散步 App。
到這裡 TraceWalk 的 30 天開發就正式結束了,恭喜我又一次完賽~