原文地址: iOS 無障礙化(適老化)適配總結(jié)
VoiceOver 和 Accessibility
iOS 開發(fā)中主要討論的是 UIAccessibility
的 API 在 VoiceOver
上的運(yùn)用.
旁白使用手冊(cè)-在 iPhone 上學(xué)習(xí)旁白手勢(shì)
檢測(cè)當(dāng)前是否開啟無障礙模式
系統(tǒng)通知:UIAccessibilityVoiceOverStatusDidChangeNotification
系統(tǒng)方法:UIAccessibilityIsVoiceOverRunning()
iOS代碼適配示例
UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
// 描述控件是什么 第一個(gè)播放
titleLabel.accessibilityLabel = @"標(biāo)簽";
// 元素的特征 如按鈕、鏈接等 第二個(gè)播放
titleLabel.accessibilityTraits = UIAccessibilityTraitStaticText;
// 附加說明 第三個(gè)播放, 只在真機(jī)播放, 且有一秒延遲
titleLabel.accessibilityHint = @"提示";
// 元素的frame(基于屏幕坐標(biāo)系, 一般不用填設(shè)置), 當(dāng)系統(tǒng)聚焦到當(dāng)前元素時(shí), 會(huì)有一個(gè)黑色的外框, 該值就是聚焦框的大小. 當(dāng)元素過小時(shí)可以通過設(shè)置該 frame 使得容易點(diǎn)擊, 這個(gè)不會(huì)改變 app 的 UI. 默認(rèn)為 CGRectZero.
titleLabel.accessibilityFrame = CGRectMake(0, 0, 100, 100);
// 元素的值 用在UISlider,UITextField等組件上 用來描述元素的值
titleLabel.accessibilityValue = @"60%";
// VoiceOver會(huì)把這幾個(gè)屬性連接起來, 朗讀順序?yàn)閘abel→value(可選)→traits→hint.
需要適配的場景
由于系統(tǒng)控件是默認(rèn)處理好的,而且VoiceOver的默認(rèn)閱讀順序通常也沒什么大問題,因此需要開發(fā)者專門去兼容的場景有如下
自定義 View 組件無障礙化
當(dāng)子元素需要配置成 accessible, 而你的視圖容器不需要配置成 accessible, 下面兩個(gè)方法可以解決
- 直接設(shè)置
accessibilityElements
屬性 (iOS8+), 使用在代碼里搜下吧, 很簡單. - (很老的 API 了, 不推薦使用)視圖需要實(shí)現(xiàn)
UIAccessibilityContainer
.
只有背景圖片無文字的控件
需要給出描述和對(duì)應(yīng)控件的屬性, 例如返回按鈕/關(guān)閉按鈕
self.backButton.accessibilityLabel = @"返回";
self.closeButton.accessibilityLabel = @"關(guān)閉";
self.closeButton.accessibilityTraits = UIAccessibilityTraitButton;
讀取時(shí)間的無障礙
24 小時(shí)制的時(shí)刻系統(tǒng)無障礙會(huì)按字符一個(gè)個(gè)讀出,需要轉(zhuǎn)化成 12 小時(shí)制的寫法,才會(huì)正常讀取時(shí)間,例如 22:58 要寫成 10.58PM.
某些控件的無障礙元素默認(rèn)是關(guān)閉的, 需要開啟
UIView/UIImageViewisAccessibilityElement
默認(rèn)為 NO
而 UILabel/UIButton/UISwitch/UICollectionViewCell/UITableViewCell/UIPageControl 等組件默認(rèn)為 YES
.
其中 UILabel 比較離譜, 偶現(xiàn) setText, 然后 po isAccessibilityElement 為 YES, 但是不響應(yīng)的情況, 最好都顯式設(shè)置 isAccessibilityElement = YES 吧.
父 View 如果為 AccessibilityElement, 子 element 將不響應(yīng) VoiceOver
self.bottomView.isAccessibilityElement = YES;
self.bottomView.accessibilityLabel = @"XXX";
比如一個(gè)View中有多個(gè)Label,那么每一個(gè)下面的Label單獨(dú)訪問可能意義不大,那么就可以將這個(gè)View設(shè)置成可以訪問的,然后將其accessibilityLabel設(shè)置為所有子Label的 accessibilityLabel的合并值.
無障礙控件點(diǎn)擊區(qū)域過小
類似下圖:圖片和文字搭配, 需要擴(kuò)大無障礙點(diǎn)擊范圍:設(shè)置accessibilityFrame
,通過CGRectUnion(frame1,frame2);
需要注意的是 accessibilityFrame 是相對(duì)屏幕的坐標(biāo)系的, 使用 [UIView convertRect:toView:] 來轉(zhuǎn)一次才能設(shè)置, 如果用了自動(dòng)布局會(huì)很麻煩.
UI 組件實(shí)際功能不符合時(shí)
使用 button 只做點(diǎn)擊作用的時(shí)候(不需要選中態(tài)),需要設(shè)置對(duì)應(yīng)的控件屬性 accessibilityTraits
// 1. 不需要播報(bào)"已選中"
button.accessibilityTraits &= ~UIAccessibilityTraitSelected;
// 2. 純文本
button.accessibilityTraits = UIAccessibilityTraitStaticText;
使用 laber / view / imageView, 實(shí)現(xiàn)自定義響應(yīng)事件時(shí),設(shè)置對(duì)應(yīng)控件屬性 accessibilityTraits 為 UIAccessibilityTraitButton
.
hidden 元素
有時(shí)把某個(gè) view 設(shè)成 hidden 的時(shí)候, UI 上已經(jīng)不展示了, 但是VoiceOver仍然可以讀到.
此時(shí)可以使用
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil)
強(qiáng)制更新VoiceOver的表現(xiàn).
主動(dòng)播報(bào)
toast 彈出或者刷新成功等場景需要主動(dòng)播報(bào), 如下:
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, voiceText);
彈窗
彈窗后屏蔽彈窗下面的元素
popupView.accessibilityViewIsModal = YES;//當(dāng)前view才能響應(yīng)無障礙播報(bào)
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, popupView.accessibilityElements.firstObject);
焦點(diǎn)亂跳
焦點(diǎn)亂跳有兩個(gè)可能
- UICollectionView/UITableView
reloadData
時(shí)候焦點(diǎn)會(huì)跳到最后一個(gè) Cell, 解決辦法為自行實(shí)現(xiàn) cell 的更新, 減少直接reloadData
調(diào)用; 如果這個(gè) Cell 是滿屏的, 可以設(shè)置當(dāng)前 Cell 的 accessibilityViewIsModal 簡單解決. - 某個(gè) UI 元素被加入到 accessibilityElements 但是又被標(biāo)記為不可用
isAccessibilityElement = NO
, 在手指左劃時(shí)會(huì)出現(xiàn)焦點(diǎn)亂跳(但是右劃正常)
UICollectionViewDelegate 異常
在無障礙開啟時(shí), UICollectionViewDelegate
部分回調(diào)異常, 表現(xiàn)為 Cell 提前預(yù)加載了, 不在這些方法中執(zhí)行關(guān)鍵邏輯即可, 目前已知下面兩個(gè)回調(diào)會(huì)有異常
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath;
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath;
Cell 橫劃刪除 在無障礙下無法使用
使用自定義操作來代替
UIAccessibilityCustomAction * action = [[UIAccessibilityCustomAction alloc] initWithName:@"測(cè)試" target:self selector:@selector(testToast)];
UIAccessibilityCustomAction * action1 = [[UIAccessibilityCustomAction alloc] initWithName:@"測(cè)試1" target:self selector:@selector(testToast1)];
UIAccessibilityCustomAction * action2 = [[UIAccessibilityCustomAction alloc] initWithName:@"測(cè)試2" target:self selector:@selector(testToast2)];
self.accessibilityCustomActions = @[action, action1, action2];
觸發(fā)方法:
- 單擊選中該 UI 元素
- 單指上下劃, 第一次播報(bào) "測(cè)試", 再單指上下劃第二次播報(bào) "測(cè)試2", 繼續(xù)劃會(huì)有第三次播報(bào) "測(cè)試3", 第四次播報(bào)"激活" (初始狀態(tài)).
- 然后在某一次播報(bào)后單指雙擊, 就可以觸發(fā)對(duì)應(yīng)的
selector
需要播報(bào)"已選中"
// 設(shè)置"已選中"播報(bào)
view.accessibilityTraits |= UIAccessibilityTraitSelected;
// 移除"已選中"播報(bào)
view.accessibilityTraits &= ~UIAccessibilityTraitSelected;