前言:Android 應用的目標應該是讓所有人都可以使用交汤,包括有無障礙功能需求的人士。
有視覺障礙聋涨、色盲、視覺障礙负乡、精細動作失能牍白、認知障礙以及很多其他殘疾的人員在日常生活中使用 Android 設備來完成各項任務。如果您能夠在開發(fā)應用時考慮無障礙功能抖棘,那么您便可以改善用戶體驗茂腥,尤其是對于具有這些障礙和其他無障礙功能需求的用戶來說。
在日常工作來說切省,對于一些系統(tǒng)性要求的公司最岗,也是作為必須適配項之一。
1.啟用焦點導航
Android提供了幾個API讓開發(fā)者決定用戶界面控件是否可聚焦数尿,甚至請求給控件賦予焦點:
如果視圖不是默認聚焦仑性,可以在布局文件中設置[android:focusable]
(http://developer.android.com/reference/android/view/View.html#attr_android:focusable)屬性為true
惶楼,或者調用setFocusable()方法讓視圖可聚焦右蹦。
2.基本的Android無障礙適配-contentDescription
- 對于Android的基礎組件ImageButton ImageView CheckBox等,只需要簡單的在xml中設置 android:
contentDescription
="xx"屬性或代碼中動態(tài)設置view.setContentDescription("xx")即可歼捐。 - 對于EditText區(qū)域何陆,提供
android:hint
屬性代替內容描述,文本區(qū)域為空的時候此屬性幫助用戶理解應該輸入什么樣的內容豹储。當文本區(qū)域填充上內容贷盲,TalkBack將會讀出輸入的文本,而不會讀出提示文本。 - TextView或者繼承至其的控件,如果contentDescription屬性的值為空,無障礙服務會獲取text屬性的文本信息作為語音提示巩剖。
- 一般情況下铝穷,如果無障礙服務說明的是 ViewGroup,則會將來自其子 View 的內容標簽合并在一起佳魔。要抑制此行為曙聂,并指明您希望為該項及其不可聚焦的子 View 提供自己的說明,
請在 ViewGroup 上設置 contentDescription鞠鲜。
比如有一個展示型卡片宁脊,不做任何設置時,可能實際無障礙自動播報的順序或播報的內容和預期不符合贤姆,可以format需要播報的內容榆苞,給最外層view整體設置contentDescritpion。 - 對于一個不想讓無障礙播報內容的view 想要移除其焦點霞捡,可以設置其
android:importantForAccessibility="no";
默認為yes - 希望一個view獲取talkback的焦點坐漏,可以使用方法view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
3.復雜view的無障礙適配
對于基礎組件,設置contentDescritpion就可以達到目標碧信,那對于我們自定義的復雜view(比如日歷的月盤仙畦,chart 柱狀圖等)來說,又該如何交互與播報呢音婶?
總的來說慨畸,通過在自定義view里設置 ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate); 擴展各個無障礙方法,實現(xiàn)自定義的無障礙衣式。
一個小小例子 AccessibilityDelegateSupportActivity.java
onPopulateAccessibilityEvent()方法可專門用來為事件添加或修改文本內容寸士,這些信息會被如TalkBack的無障礙服務轉化為音頻反饋。
onInitializeAccessibilityNodeInfo()方法填充AccessibilityNodeInfo對象碴卧,視圖層次在接收此事件后生成無障礙事件弱卡,無障礙服務使用AccessibilityNodeInfo對象訪問該視圖層次,獲得更多的上下文信息并為用戶提供合適的反饋住册。-
一個詳細講的demo樣例:
這是一個 柱狀圖 橫坐標代表的是24小時婶博,縱軸是一些數(shù)據(jù)的展示,可忽略∮桑現(xiàn)在的需求是在用戶無障礙播報時可選中每小時對應的柱子
凡人,播報當前小時以及該小時的具體內容。如果不做任何限制叹阔,當前是不會有焦點到這個自定義的柱子上的挠轴。
附上關鍵代碼
public class DayColumnChart extends View {
private MyAccessHelper mAccessHelper;//無障礙代理
private List<DayColumnData> mColumnData;//每小時對應的數(shù)據(jù)
public DayColumnChart(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
....
....
private void init() {
mAccessHelper = new MyAccessHelper(this);
ViewCompat.setAccessibilityDelegate(this, mAccessHelper);
}
....
....
@Override
public boolean dispatchHoverEvent(MotionEvent event) {
return mAccessHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
}
}
private class MyAccessHelper extends ExploreByTouchHelper {
private Rect mAccessRect;
/**
* Constructs a new helper that can expose a virtual view hierarchy for the specified host
* view.
*
* @param host view whose virtual view hierarchy is exposed by this helper
*/
MyAccessHelper(@NonNull View host) {
super(host);
mAccessRect = new Rect();
}
@Override
protected int getVirtualViewAt(float x, float y) {
checkSelectIndex(x, y);
return mSelectIndex;
}
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
if (!ArrayUtils.isEmpty(mColumnData)) {
for (final DayColumnData columnDatum : mColumnData) {
virtualViewIds.add(columnDatum.getIndex());
}
}
}
@Override
protected void onPopulateNodeForVirtualView(int virtualViewId,
@NonNull AccessibilityNodeInfoCompat node) {
if (ArrayUtils.isEmpty(mColumnData) || getData(mSelectIndex) == null) {
mAccessRect.setEmpty();
node.setBoundsInParent(mAccessRect);
node.setEnabled(false);
node.setContentDescription("");
return;
}
DayColumnData dayColumnData = Objects.requireNonNull(getData(virtualViewId));
int startX = mFirstColumnMarginStart + (virtualViewId - mStartIndex) * (mSpaceWidth
+ mColumnWidth);
int endX = startX + mColumnWidth;
mAccessRect.set(startX, mLineTopY, endX, mLineBottomY);
node.setBoundsInParent(mAccessRect);
int max = dayColumnData.getMax();
int min = dayColumnData.getMin();
node.setClickable(true);
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
final Resources resources = getResources();
String contentDescription = resources.getString(R.string.tb_blood_pressure_day_chart,
resources.getString(R.string.hour_in_day_format, virtualViewId), max, min);
node.setContentDescription(contentDescription);
}
private DayColumnData getData(int selectIndex) {
for (final DayColumnData data : mColumnData) {
if (data.getIndex() == selectIndex) {
return data;
}
}
return null;
}
@Override
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
@Nullable Bundle arguments) {
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK
&& mChartHelper != null && mOnColumnClickListener != null) {
mOnColumnClickListener.onClick(mSelectIndex);
}
return false;
}
}
- ViewCompat.setAccessibilityDelegate(this, new MyAccessHelper(this));設置處理無障礙的代理
- 較好實現(xiàn)無障礙的方式是借助
ExploreByTouchHelper。
(主要參考了Android 5.1系統(tǒng)源碼中LockPatternView類的無障礙實現(xiàn))編寫相應的ExploreByTouchHelper類耳幢,重載必要的方法實現(xiàn)自定義view無障礙岸晦。
- int getVirtualViewAt(float x, float y) x.y也就是我們處理onTouchEvent時獲取的x,y 當有觸摸事件時,根據(jù)x,y 返回當前是哪個結點,返回的int值由自己約定(和getVisibleVirtualViews方法對應启上,約定index)
- getVisibleVirtualViews(List<Integer> virtualViewIds) 添加想要聚焦的index 比如示例里 我只添加了有數(shù)據(jù)的分時柱子上邢隧,比如8,10,22 就代表8點,10點,22點 這三個有數(shù)據(jù)的位置需要播報無障礙.
- void onPopulateNodeForVirtualView(int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) 給虛擬View設置描述文本和邊框。在view刷新時冈在,會遍歷getVisibleVirtualViews我們添加的結點index府框,調用onPopulateNodeForVirtualView將焦點聚集,在node上設置要播報的內容,(還可以為其添加點擊事件,添加的事件要在onPerformActionForVirtualView)處理讥邻,而setBoundsInParent方法傳入一個rect,邊框是指無障礙模式下選中的區(qū)塊邊界迫靖。
- onPerformActionForVirtualView 提供交互,觸發(fā)回調重繪控件
- 重寫dispatchHoverEvent事件 處理以及發(fā)送事件
精選參考文章:
無障礙學習整理(基于talkback)
從源碼看Accessibility事件分發(fā)流程
無障礙功能概覽 讓應用無障礙_中文版對應 構建無障礙服務|Android開發(fā)
360烽火實驗室 Android Accessibility安全性研究報告