歡迎關注微信公眾號「隨手記技術團隊」,檢視更多隨手記團隊的技術文章。轉載請註明出處
本文作者:鄧慧
原文連結:mp.weixin.qq.com/s/YckeGC_ZU…
隨手記Android無障礙實踐
前言
根據統計,目前我國有1700多萬視障人士,意味著平均每81人中就有一位視障人士可能會在使用網際網路服務時遇到困難。目前隨手記擁有3億註冊使用者,為了讓財務金融服務惠及每一位使用者,幫助視障人士輕鬆地進行記賬、投資和學習財商知識,讓他們能平等、方便、無障礙地獲取資訊和利用資訊,我們對隨手記Android進行了無障礙改造和優化。
無障礙指南
Android產品的無障礙主要是針對視覺障礙人士,在裝置的輔助功能中開啟無障礙服務(如TalkBack)後,它能夠讀取螢幕上的文字資訊,轉化為語音提示,達到資訊無障礙。
規範細則
- 所有View應統一通過contentDescription屬性加上標籤
- 文字標籤要有意義
- 裝飾性的UI元素需要去掉標籤和焦點
- EditText需通過hint屬性設定標籤
- 觸控目標大小至少為48*48dp,觸控目標間距至少為8dp
- 應將相關的、有相同響應的元素組合在一起
- 焦點切換順序應遵循視覺順序,從左到右,從上到下
- 較複雜的頁面應採取分組聚焦的形式,減少細粒度
- 自定義的控制元件需要進行無障礙改造
WCAG 2.0四大原則
- 可感知性:資訊和使用者介面元件必須以可感知的方式呈現給使用者。
- 可操作性:使用者介面元件和導航必須可操作。
- 可理解性:資訊和使用者介面操作必須是可理解的。
- 魯棒性:內容必須健壯到可信地被種類繁多的使用者代理(包括輔助技術) 所解釋。
開啟無障礙服務
- 下載安裝TalkBack軟體(有些系統自帶),它能讀取螢幕中的文字資訊
- 保證有文字轉語音(TTS)輸出引擎,通常手機會自帶一個,另外也可以下載訊飛語記
- 進入設定 -> 輔助功能(或高階選項) -> 找到TalkBack服務並開啟
當出現綠區域並伴有語音提示的時候表示進入了無障礙模式。View能被正常選中,並有語音提示其文字資訊,說明該View具有無障礙功能。
操作方式有所改變
- 單擊,選中某個具有焦點的View(綠區域)
- 雙擊相當於正常模式下的點選(啟動、進入等)
- 滑動,需要雙指往上、下、左、右
實戰例項
1.給UI元素新增標籤
找到介面中所有有效的元素,設定文字資訊。
簡單程式碼示例:
// XML
<ImageButton
...
android:contentDescription="@string/share" />
複製程式碼
// 程式碼
private void updateImageButton() {
if (mediaCurrentlyPlaying) {
playPauseImageView.setContentDescription(getString(R.string.pause));
} else {
playPauseImageView.setContentDescription(getString(R.string.play));
}
}
複製程式碼
1.1 正確新增標籤
- TextView或者繼承至其的控制元件,如果contentDescription屬性的值為空,無障礙服務會獲取text屬性的文字資訊作為語音提示。
- EditText,需設定hint屬性的值
- 其它控制元件(如ImageView、ImageButton)需要通過設定contentDescription的值
1.2 提供清晰和有意義的標籤文字
- 力求精確、簡潔
- 避免在描述文字中包含型別和狀態
- 指明元素功能,而不是描述圖示
- 狀態改變或功能改變,標籤需隨之改變
2.改造非標準元件的選中狀態
如上,有些介面的選中狀態是通過設定ImageView的背景圖片來控制的。無障礙服務無法識別,語音提示中不包含選中狀態。 處理方法: 一、使用可以朗讀選中狀態的系統標準控制元件,如CheckBox或CheckedTextView。 二、給控制元件新增無障礙代理(AccessibilityDelegate),在onInitializeAccessibilityNodeInfo()方法中呼叫AccessibilityNodeInfo物件的setChecked方法設定選中狀態。 我們使用的是第二種方式。具體實現如下:rootView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setCheckable(true);
info.setChecked(itemData.isSelected());
}
});
複製程式碼
3.焦點處理
圖中第三方登入的微信圖示和文字分別具有焦點,需要整合到一起。避免多餘的操作,加快瀏覽。對於類似手機快捷註冊文字按鈕,應該擴大可觸碰範圍。有些介面包含裝飾性的元素,需要去除掉焦點。 例如:隨手記更多介面的間隔塊。
移除焦點程式碼示例:
android:focusable="false"
android:focusableInTouchMode="false"
android:importantForAccessibility="no"
複製程式碼
4.自定義View的改造
4.1 如下圖記一筆中的滾輪,未處理時在無障礙模式下無法使用。
改造過程: 1.先設定滾輪皮膚的焦點,保證可選中。2.在滾輪Item選中的回撥函式中,設定view的contentDescription屬性同時傳送無障礙事件。
// 防止頻率過高,做了延時處理
private void sendAccessibilityViewSelectedEvent() {
postDelayed(new Runnable() {
@Override
public void run() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
}
}, 200L);
}
複製程式碼
3.過載onPopulateAccessibilityEvent方法,新增描述文字
@Override
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
super.onPopulateAccessibilityEvent(event);
int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED
|| eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
if (viewAdapter != null) {
event.getText().add(viewAdapter.getItemContentDes(currentItem));
event.setItemCount(viewAdapter.getItemsCount());
event.setCurrentItemIndex(currentItem);
}
}
}
複製程式碼
4.2 手勢皮膚改造
沒處理之前就是一個塊區,滑動沒反應。
較好實現無障礙的方式是藉助ExploreByTouchHelper。(主要參考了Android 5.1系統原始碼中LockPatternView類的無障礙實現) 下面給出了部分程式碼實現: 1.編寫相應的ExploreByTouchHelper類,過載6個方法
private final class PatternExploreByTouchHelper extends ExploreByTouchHelper {
private Rect mTempRect = new Rect();
private HashMap<Integer, VirtualViewContainer> mItems = new HashMap<>();
private static final int VIRTUAL_BASE_VIEW_ID = 1;
/**
* 手勢皮膚有9個點,每個點都做為一個虛擬節點,要根據x,y座標獲取對應的虛擬節點的編號(這個int值由自己約定)
* @return 其它返回ExploreByTouchHelper.INVALID_ID
*/
@Override
protected int getVirtualViewAt(float x, float y) {
final int rowHit = getRowHit(y);
if (rowHit < 0) {
return ExploreByTouchHelper.INVALID_ID;
}
final int columnHit = getColumnHit(x);
if (columnHit < 0) {
return ExploreByTouchHelper.INVALID_ID;
}
boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit];
int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID;
int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
return view;
}
/**
* 方法名有點奇怪,它的作用是把虛擬節點的編號放進List中
* 這裡我們加了9個編號進來,1到9
* @param virtualViewIds
*/
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
if (!mPatternInProgress) {
return;
}
for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
if (!mItems.containsKey(i)) {
VirtualViewContainer item = new VirtualViewContainer(getTextForVirtualView(i));
mItems.put(i, item);
}
virtualViewIds.add(i);
}
}
/**
* 給每個虛擬節點填充事件,即手勢皮膚中的9個點設定描述文字
*/
@Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
if (mItems.containsKey(virtualViewId)) {
CharSequence contentDescription = mItems.get(virtualViewId).description;
event.getText().add(contentDescription);
}
}
/**
* 給宿主View填充事件,即手勢皮膚設定描述文字
*/
@Override
public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
super.onPopulateAccessibilityEvent(host, event);
if (!mPatternInProgress) {
CharSequence contentDescription = getContext().getText(R.string.lock_pattern_area);
event.setContentDescription(contentDescription);
}
}
/**
* 給虛擬View設定描述文字和邊框
* 邊框是指無障礙模式下選中的區塊邊界
* @param virtualViewId
* @param node
*/
@Override
protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node) {
node.setText(getTextForVirtualView(virtualViewId));
node.setContentDescription(getTextForVirtualView(virtualViewId));
if (mPatternInProgress) {
node.setFocusable(true);
if (isClickable(virtualViewId)) {
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
node.setClickable(isClickable(virtualViewId));
}
}
final Rect bounds = getBoundsForVirtualView(virtualViewId);
node.setBoundsInParent(bounds);
}
/**
* 提供互動,觸發回撥重繪控制元件
*/
@Override
protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) {
switch (action) {
case AccessibilityNodeInfo.ACTION_CLICK:
return onItemClicked(virtualViewId);
default:
break;
}
return false;
}
// ...
}
複製程式碼
2.在建構函式中設定無障礙代理
public LockPatternView(Context context) {
// something else
// ...
// 無障礙代理
mPatternTouchHelper = new PatternExploreByTouchHelper(this);
ViewCompat.setAccessibilityDelegate(this, mPatternTouchHelper);
}
複製程式碼
3.在LockPatternView中實現onHoverEvent()和dispatchHoverEvent()
@Override
public boolean onHoverEvent(MotionEvent event) {
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_HOVER_ENTER:
event.setAction(MotionEvent.ACTION_DOWN);
break;
case MotionEvent.ACTION_HOVER_MOVE:
event.setAction(MotionEvent.ACTION_MOVE);
break;
case MotionEvent.ACTION_HOVER_EXIT:
event.setAction(MotionEvent.ACTION_UP);
break;
case MotionEvent.ACTION_CANCEL:
event.setAction(MotionEvent.ACTION_CANCEL);
}
onTouchEvent(event);
event.setAction(action);
return super.onHoverEvent(event);
}
@Override
protected boolean dispatchHoverEvent(MotionEvent event) {
boolean handled = super.dispatchHoverEvent(event);
handled |= mPatternTouchHelper.dispatchHoverEvent(event);
return handled;
}
複製程式碼
4.手勢狀態(如完成、中斷等)的回撥函式中要呼叫announceForAccessibility()提示使用者。
總結
在實現無障礙的同時,也解決了自定義View的UI自動化測試問題。無障礙需要不斷更新迭代、優化。對此團隊也制定了無障礙編碼規範,列入程式碼審查要點中,來保證產品持續提供良好的無障礙功能。