Adding scroll actions to accessibility node info.
1. Scrolling actions are crucial for enabling a gesture based
traversal of the UI and specifically scrollable containers
especially lists and anything backed by an adapter. Since
accessibility focus can land only attached views, it cannot
visit views for adapter items not shown on the screen.
Auto scrolling the list as a result of putting access focus
ot a list item does not work well since the user may get
trapped in a long list. Adding an accessibility node provider
to emit virtual views for one view before the first and one
after the last is complex and suffers the limitation of trapping
the user. Accessibility service need an explicit scroll actions
which may be performed upon an explicit user action. Hence,
the user is informed for the start/end of the visible part of
the list and he makes a deliberate choice to scroll. This will
benefit also people developing Braille devices since they can
scroll the content without telling the user to stop using the
Braille controller and take the device out of his pocket to scroll
and go back to the Braille controller.
NOTE: Without these action large portions of the screen will be
hard to access since users will have to touch and explore to
find and scroll the list.
Change-Id: Iafcf54d4967893205872b3649025a4e347a299ed
diff --git a/api/current.txt b/api/current.txt
index 4b10577..c909ba0 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -25234,6 +25234,8 @@
field public static final int ACTION_NEXT_HTML_ELEMENT = 1024; // 0x400
field public static final int ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY = 512; // 0x200
field public static final int ACTION_PREVIOUS_HTML_ELEMENT = 2048; // 0x800
+ field public static final int ACTION_SCROLL_BACKWARD = 8192; // 0x2000
+ field public static final int ACTION_SCROLL_FORWARD = 4096; // 0x1000
field public static final int ACTION_SELECT = 4; // 0x4
field public static final android.os.Parcelable.Creator CREATOR;
field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 6b14ba5..0517d4b 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -205,6 +205,16 @@
public static final int ACTION_PREVIOUS_HTML_ELEMENT = 0x00000800;
/**
+ * Action to scroll the node content forward.
+ */
+ public static final int ACTION_SCROLL_FORWARD = 0x00001000;
+
+ /**
+ * Action to scroll the node content backward.
+ */
+ public static final int ACTION_SCROLL_BACKWARD = 0x00002000;
+
+ /**
* Argument for which movement granularity to be used when traversing the node text.
* <p>
* <strong>Type:</strong> int<br>
@@ -569,6 +579,16 @@
* @see AccessibilityNodeInfo#ACTION_CLEAR_FOCUS
* @see AccessibilityNodeInfo#ACTION_SELECT
* @see AccessibilityNodeInfo#ACTION_CLEAR_SELECTION
+ * @see AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS
+ * @see AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS
+ * @see AccessibilityNodeInfo#ACTION_CLICK
+ * @see AccessibilityNodeInfo#ACTION_LONG_CLICK
+ * @see AccessibilityNodeInfo#ACTION_NEXT_AT_MOVEMENT_GRANULARITY
+ * @see AccessibilityNodeInfo#ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY
+ * @see AccessibilityNodeInfo#ACTION_NEXT_HTML_ELEMENT
+ * @see AccessibilityNodeInfo#ACTION_PREVIOUS_HTML_ELEMENT
+ * @see AccessibilityNodeInfo#ACTION_SCROLL_FORWARD
+ * @see AccessibilityNodeInfo#ACTION_SCROLL_BACKWARD
*/
public int getActions() {
return mActions;
@@ -1578,6 +1598,10 @@
return "ACTION_NEXT_HTML_ELEMENT";
case ACTION_PREVIOUS_HTML_ELEMENT:
return "ACTION_PREVIOUS_HTML_ELEMENT";
+ case ACTION_SCROLL_FORWARD:
+ return "ACTION_SCROLL_FORWARD";
+ case ACTION_SCROLL_BACKWARD:
+ return "ACTION_SCROLL_BACKWARD";
default:
throw new IllegalArgumentException("Unknown action: " + action);
}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 3aafba5..c4e1bf5 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -1355,6 +1355,33 @@
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(AbsListView.class.getName());
+ if (getFirstVisiblePosition() > 0) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ }
+ if (getLastVisiblePosition() < getCount() - 1) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(int action, Bundle arguments) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
+ if (getLastVisiblePosition() < getCount() - 1) {
+ final int viewportHeight = getHeight() - mListPadding.top - mListPadding.bottom;
+ smoothScrollBy(viewportHeight, PositionScroller.SCROLL_DURATION);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+ if (mFirstPosition > 0) {
+ final int viewportHeight = getHeight() - mListPadding.top - mListPadding.bottom;
+ smoothScrollBy(-viewportHeight, PositionScroller.SCROLL_DURATION);
+ return true;
+ }
+ } return false;
+ }
+ return super.performAccessibilityAction(action, arguments);
}
/**
diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java
index 1986450..ffabd1d9 100644
--- a/core/java/android/widget/HorizontalScrollView.java
+++ b/core/java/android/widget/HorizontalScrollView.java
@@ -20,6 +20,7 @@
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
+import android.os.Bundle;
import android.util.AttributeSet;
import android.view.FocusFinder;
import android.view.InputDevice;
@@ -737,10 +738,42 @@
}
@Override
+ public boolean performAccessibilityAction(int action, Bundle arguments) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
+ final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
+ final int targetScrollX = Math.min(mScrollX + viewportWidth, getScrollRange());
+ if (targetScrollX != mScrollX) {
+ smoothScrollTo(targetScrollX, 0);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+ final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
+ final int targetScrollX = Math.max(0, mScrollX - viewportWidth);
+ if (targetScrollX != mScrollX) {
+ smoothScrollTo(targetScrollX, 0);
+ return true;
+ }
+ } return false;
+ }
+ return super.performAccessibilityAction(action, arguments);
+ }
+
+ @Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(HorizontalScrollView.class.getName());
- info.setScrollable(getScrollRange() > 0);
+ final int scrollRange = getScrollRange();
+ if (scrollRange > 0) {
+ info.setScrollable(true);
+ if (mScrollX > 0) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ }
+ if (mScrollX < scrollRange) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ }
+ }
}
@Override
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index f912c66..b398ce4 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -22,6 +22,7 @@
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
+import android.os.Bundle;
import android.os.StrictMode;
import android.util.AttributeSet;
import android.view.FocusFinder;
@@ -740,10 +741,42 @@
}
@Override
+ public boolean performAccessibilityAction(int action, Bundle arguments) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
+ final int viewportHeight = getHeight() - mPaddingBottom - mPaddingTop;
+ final int targetScrollY = Math.min(mScrollY + viewportHeight, getScrollRange());
+ if (targetScrollY != mScrollY) {
+ smoothScrollTo(0, targetScrollY);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+ final int viewportHeight = getHeight() - mPaddingBottom - mPaddingTop;
+ final int targetScrollY = Math.max(mScrollY - viewportHeight, 0);
+ if (targetScrollY != mScrollY) {
+ smoothScrollTo(0, targetScrollY);
+ return true;
+ }
+ } return false;
+ }
+ return super.performAccessibilityAction(action, arguments);
+ }
+
+ @Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(ScrollView.class.getName());
- info.setScrollable(getScrollRange() > 0);
+ final int scrollRange = getScrollRange();
+ if (scrollRange > 0) {
+ info.setScrollable(true);
+ if (mScrollY > 0) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ }
+ if (mScrollY < scrollRange) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ }
+ }
}
@Override
diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
index df7bbf2..0c6d85d 100644
--- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1701,14 +1701,9 @@
| AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY
| AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY
| AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT
- | AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT;
-
- private static final int VALID_GRANULARITIES =
- AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
- | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
- | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE
- | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH
- | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE;
+ | AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT
+ | AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
+ | AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
private static final int RETRIEVAL_ALLOWING_EVENT_TYPES =
AccessibilityEvent.TYPE_VIEW_CLICKED