Made ACTION_SCROLL_IN_DIRECTION work with spans when scrolling left
Test: Added to GridLayoutManagerTest.java
Bug: 268487724
Change-Id: Ia6a422bf061f51ff3bf17faf6030a1cc743cc520
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
index 626819f..6b90c48 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
@@ -67,7 +67,6 @@
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -1355,24 +1354,69 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
- public void performActionScrollInDirection_focusLeft_vertical_withAvailableTarget()
+ public void performActionScrollInDirection_focusLeft_vertical_scrollTargetOnTheSameRow()
throws Throwable {
// TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
// earlier android version.
final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
- /*
+ /*
This generates the following grid:
1 2 3
4
*/
- runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_LEFT,
- new HashMap<Integer, String>() {{
- put(1, "Item (1)");
- put(2, "Item (2)");
- put(3, "Item (3)");
- }});
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(1));
+ runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (1)",
+ Pair.create(0, 0));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusLeft_vertical_scrollTargetOnAPreviousRow()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
+ /*
+ This generates the following grid:
+ 1 2 3
+ 4 5
+ */
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(3));
+ runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (3)",
+ Pair.create(0, 2));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusLeft_vertical_traversingThroughASpan()
+ throws Throwable {
+
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android version.
+
+ final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
+ setUpRecyclerViewAndGridLayoutManager(4, VERTICAL);
+ mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ if (position == 1) {
+ return 2;
+ }
+ return 1;
+ }
+ });
+ waitForFirstLayout(mRecyclerView);
+ /*
+ This generates the following grid:
+ 1 2 2
+ 3 4
+ */
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(2));
+ runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (2)",
+ Pair.create(0, 1));
}
@Test
@@ -1382,36 +1426,101 @@
// TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
// earlier android version.
- final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
/*
This generates the following grid:
1 2 3
- 4
+ 4 5
*/
- runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_LEFT,
- Collections.singletonList(0));
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
+ runScrollInDirectionAndFail(View.FOCUS_LEFT, Pair.create(0, 0));
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
- public void performActionScrollInDirection_focusLeft_horizontal_withAvailableTarget()
+ public void performActionScrollInDirection_focusLeft_horizontal_scrollTargetOnTheSameRow()
throws Throwable {
// TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
// earlier android versions.
- final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, HORIZONTAL);
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
/*
This generates the following grid:
1 4
- 2
+ 2 5
3
*/
- runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_LEFT,
- new HashMap<Integer, String>() {{
- put(1, "Item (4)");
- put(2, "Item (2)");
- put(3, "Item (1)");
- }});
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(4));
+ runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (2)" ,
+ Pair.create(1, 0));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusLeft_horizontal_traversingThroughASpan()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
+ setUpRecyclerViewAndGridLayoutManager(8, HORIZONTAL);
+ mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ if (position == 4) {
+ return 2;
+ }
+ return 1;
+ }
+ });
+ waitForFirstLayout(mRecyclerView);
+ /*
+ This generates the following grid:
+ 1 4 6
+ 2 5 7
+ 3 5 8
+ */
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(7));
+ runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (5)" ,
+ Pair.create(2, 1));
+
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(4));
+ runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (3)" ,
+ Pair.create(2, 0));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void performActionScrollInDirection_focusLeft_horizontal_withWrapAround()
+ throws Throwable {
+ // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+ // earlier android versions.
+
+ final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
+ setUpRecyclerViewAndGridLayoutManager(8, HORIZONTAL);
+ mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ if (position == 6) {
+ return 2;
+ }
+ return 1;
+ }
+ });
+ waitForFirstLayout(mRecyclerView);
+ /*
+ This generates the following grid:
+ 1 4 7
+ 2 5 7
+ 3 6 8
+ */
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(2));
+ runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (7)" ,
+ Pair.create(1, 2));
+
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(6));
+ runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (5)" ,
+ Pair.create(1, 1));
}
@Test
@@ -1421,15 +1530,15 @@
// TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
// earlier android versions.
- final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, HORIZONTAL);
+ final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
/*
This generates the following grid:
1 4
- 2
+ 2 5
3
*/
- runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_LEFT,
- Collections.singletonList(0));
+ setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
+ runScrollInDirectionAndFail(View.FOCUS_LEFT, Pair.create(0, 0));
}
@Test
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
index 5ab22e0..a5df2db 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
@@ -296,16 +296,17 @@
int scrollTargetPosition;
+ int row = (mRowWithAccessibilityFocus == INVALID_POSITION) ? startingRow
+ : mRowWithAccessibilityFocus;
+ int column = (mColumnWithAccessibilityFocus == INVALID_POSITION)
+ ? startingColumn : mColumnWithAccessibilityFocus;
+
switch (direction) {
case View.FOCUS_LEFT:
- scrollTargetPosition = findScrollTargetPositionOnTheLeft(startingRow,
- startingColumn, startingAdapterPosition);
+ scrollTargetPosition = findScrollTargetPositionOnTheLeft(row, column,
+ startingAdapterPosition);
break;
case View.FOCUS_RIGHT:
- int row = (mRowWithAccessibilityFocus == INVALID_POSITION) ? startingRow
- : mRowWithAccessibilityFocus;
- int column = (mColumnWithAccessibilityFocus == INVALID_POSITION)
- ? startingColumn : mColumnWithAccessibilityFocus;
scrollTargetPosition =
findScrollTargetPositionOnTheRight(row, column,
startingAdapterPosition);
@@ -461,25 +462,37 @@
return INVALID_POSITION;
}
- // Canonical case: target is on the same row. TODO (b/268487724): handle RTL.
- if (currentRow == startingRow && currentColumn < startingColumn) {
- return i;
- } else {
- if (mOrientation == VERTICAL) {
- /*
- * Grids with vertical layouts are laid out row by row...
- * 1 2 3
- * 4 5 6
- * 7 8
- * ... and the scroll target may lie on a preceding row.
- */
- if (currentRow < startingRow) {
- scrollTargetPosition = i;
- break;
- }
- } else { // HORIZONTAL
- // TODO (b/268487724): handle case where the scroll target spans multiple
- // rows/columns.
+ if (mOrientation == VERTICAL) {
+ /*
+ * For grids with vertical orientation...
+ * 1 2 3
+ * 4 5 5
+ * 6 7
+ * ... the scroll target may lie on the same or a preceding row.
+ */
+ // TODO (b/268487724): handle RTL.
+ if ((currentRow == startingRow && currentColumn < startingColumn)
+ || (currentRow < startingRow)) {
+ scrollTargetPosition = i;
+ mRowWithAccessibilityFocus = currentRow;
+ mColumnWithAccessibilityFocus = currentColumn;
+ break;
+ }
+ } else { // HORIZONTAL
+ /*
+ * For grids with horizontal orientation, the scroll target may span multiple
+ * rows. For example, in this grid...
+ * 1 4 6
+ * 2 5 7
+ * 3 5 8
+ * ... moving from 8 to 5 or from 7 to 5 is considered staying on the "same row"
+ * because the row indices for 5 include 8's and 7's row.
+ */
+ if (getRowIndices(i).contains(startingRow) && currentColumn < startingColumn) {
+ // Note: mRowWithAccessibilityFocus not updated since the scroll target is on
+ // the same row.
+ mColumnWithAccessibilityFocus = currentColumn;
+ return i;
}
}
}
@@ -564,22 +577,35 @@
// ... the generated map - {2 -> 5, 1 -> 7, 0 -> 6} - can be used to scroll from,
// say, "2" (adapter position 1) in the second row to "7" (adapter position 6) in the
// preceding row.
+ //
+ // Sometimes cells span multiple rows. In this example:
+ // 1 4 7
+ // 2 5 7
+ // 3 6 8
+ // ... the generated map - {0 -> 6, 1 -> 6, 2 -> 7} - can be used to scroll left from,
+ // say, "3" (adapter position 2) in the third row to "7" (adapter position 6) on the
+ // second row, and then to "5" (adapter position 4).
Map<Integer, Integer> rowToLastItemPositionMap = new TreeMap<>(Collections.reverseOrder());
for (int position = 0; position < getItemCount(); position++) {
- int row = getRowIndex(position);
- if (row < 0) {
- if (DEBUG) {
- throw new RuntimeException(
- "row equals " + row + ". It cannot be less than zero");
+ Set<Integer> rows = getRowIndices(position);
+ for (int row: rows) {
+ if (row < 0) {
+ if (DEBUG) {
+ throw new RuntimeException(
+ "row equals " + row + ". It cannot be less than zero");
+ }
+ return INVALID_POSITION;
}
- return INVALID_POSITION;
+ rowToLastItemPositionMap.put(row, position);
}
- rowToLastItemPositionMap.put(row, position);
}
for (int row : rowToLastItemPositionMap.keySet()) {
if (row < startingRow) {
- return rowToLastItemPositionMap.get(row);
+ int scrollTargetPosition = rowToLastItemPositionMap.get(row);
+ mRowWithAccessibilityFocus = row;
+ mColumnWithAccessibilityFocus = getColumnIndex(scrollTargetPosition);
+ return scrollTargetPosition;
}
}
return INVALID_POSITION;