Allowing association between a view and its label for accessibility.
1. For accessibility purposes it is important to be able to associate
a view with content with a view that labels it. For example, if
an accessibility service knows that a TextView is associated with
an EditText, it can provide much richer feedback.
This change adds APIs for setting a view to be the label for another
one and setting the label for a view, i.e. the reverse association.
bug:5016937
Change-Id: I7b837265c5ed9302e3ce352396dc6e88413038b5
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 8f77663..03f9b72 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -2759,6 +2759,23 @@
private CharSequence mContentDescription;
/**
+ * Specifies the id of a view for which this view serves as a label for
+ * accessibility purposes.
+ */
+ private int mLabelForId = View.NO_ID;
+
+ /**
+ * Predicate for matching labeled view id with its label for
+ * accessibility purposes.
+ */
+ private MatchLabelForPredicate mMatchLabelForPredicate;
+
+ /**
+ * Predicate for matching a view by its id.
+ */
+ private MatchIdPredicate mMatchIdPredicate;
+
+ /**
* Cache the paddingRight set by the user to append to the scrollbar's size.
*
* @hide
@@ -3370,6 +3387,9 @@
case com.android.internal.R.styleable.View_contentDescription:
setContentDescription(a.getString(attr));
break;
+ case com.android.internal.R.styleable.View_labelFor:
+ setLabelFor(a.getResourceId(attr, NO_ID));
+ break;
case com.android.internal.R.styleable.View_soundEffectsEnabled:
if (!a.getBoolean(attr, true)) {
viewFlagValues &= ~SOUND_EFFECTS_ENABLED;
@@ -4837,6 +4857,28 @@
info.setParent((View) parent);
}
+ if (mID != View.NO_ID) {
+ View rootView = getRootView();
+ if (rootView == null) {
+ rootView = this;
+ }
+ View label = rootView.findLabelForView(this, mID);
+ if (label != null) {
+ info.setLabeledBy(label);
+ }
+ }
+
+ if (mLabelForId != View.NO_ID) {
+ View rootView = getRootView();
+ if (rootView == null) {
+ rootView = this;
+ }
+ View labeled = rootView.findViewInsideOutShouldExist(this, mLabelForId);
+ if (labeled != null) {
+ info.setLabelFor(labeled);
+ }
+ }
+
info.setVisibleToUser(isVisibleToUser());
info.setPackageName(mContext.getPackageName());
@@ -4888,6 +4930,14 @@
}
}
+ private View findLabelForView(View view, int labeledId) {
+ if (mMatchLabelForPredicate == null) {
+ mMatchLabelForPredicate = new MatchLabelForPredicate();
+ }
+ mMatchLabelForPredicate.mLabeledId = labeledId;
+ return findViewByPredicateInsideOut(view, mMatchLabelForPredicate);
+ }
+
/**
* Computes whether this view is visible to the user. Such a view is
* attached, visible, all its predecessors are visible, it is not clipped
@@ -5059,6 +5109,32 @@
}
/**
+ * Gets the id of a view for which this view serves as a label for
+ * accessibility purposes.
+ *
+ * @return The labeled view id.
+ */
+ @ViewDebug.ExportedProperty(category = "accessibility")
+ public int getLabelFor() {
+ return mLabelForId;
+ }
+
+ /**
+ * Sets the id of a view for which this view serves as a label for
+ * accessibility purposes.
+ *
+ * @param id The labeled view id.
+ */
+ @RemotableViewMethod
+ public void setLabelFor(int id) {
+ mLabelForId = id;
+ if (mLabelForId != View.NO_ID
+ && mID == View.NO_ID) {
+ mID = generateViewId();
+ }
+ }
+
+ /**
* Invoked whenever this view loses focus, either by losing window focus or by losing
* focus within its window. This method can be used to clear any state tied to the
* focus. For instance, if a button is held pressed with the trackball and the window
@@ -6110,17 +6186,14 @@
return null;
}
- private View findViewInsideOutShouldExist(View root, final int childViewId) {
- View result = root.findViewByPredicateInsideOut(this, new Predicate<View>() {
- @Override
- public boolean apply(View t) {
- return t.mID == childViewId;
- }
- });
-
+ private View findViewInsideOutShouldExist(View root, int id) {
+ if (mMatchIdPredicate == null) {
+ mMatchIdPredicate = new MatchIdPredicate();
+ }
+ mMatchIdPredicate.mId = id;
+ View result = root.findViewByPredicateInsideOut(this, mMatchIdPredicate);
if (result == null) {
- Log.w(VIEW_LOG_TAG, "couldn't find next focus view specified "
- + "by user for id " + childViewId);
+ Log.w(VIEW_LOG_TAG, "couldn't find view with id " + id);
}
return result;
}
@@ -14922,6 +14995,9 @@
*/
public void setId(int id) {
mID = id;
+ if (mID == View.NO_ID && mLabelForId != View.NO_ID) {
+ mID = generateViewId();
+ }
}
/**
@@ -18008,4 +18084,22 @@
return null;
}
}
+
+ private class MatchIdPredicate implements Predicate<View> {
+ public int mId;
+
+ @Override
+ public boolean apply(View view) {
+ return (view.mID == mId);
+ }
+ }
+
+ private class MatchLabelForPredicate implements Predicate<View> {
+ private int mLabeledId;
+
+ @Override
+ public boolean apply(View view) {
+ return (view.mLabelForId == mLabeledId);
+ }
+ }
}