Merge "Introduce rotary container" into rvc-qpr-dev
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java
index dce68c5..8ae6de7 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java
@@ -151,7 +151,7 @@
ViewGroup parent = (ViewGroup) firstItem.getParent();
parent.removeView(firstItem);
- assertThat(mList.isFocused()).isTrue();
+ assertThat(mFocusedByDefault.isFocused()).isTrue();
}
}));
}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
index ea7c855..06ff9fd 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
@@ -113,6 +113,7 @@
@Override
public void onGlobalLayout() {
mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ CarUiUtils.setRotaryScrollEnabled(mList5, /* isVertical= */ true);
View firstItem = mList5.getLayoutManager().findViewByPosition(0);
assertThat(ViewUtils.getAncestorScrollableContainer(firstItem))
.isEqualTo(mList5);
@@ -360,12 +361,25 @@
}
@Test
+ public void testRequestFocus_rotaryContainer() {
+ mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ assertRequestFocus(mList5, false);
+ }
+ }));
+ }
+
+ @Test
public void testRequestFocus_scrollableContainer() {
mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ CarUiUtils.setRotaryScrollEnabled(mList5, /* isVertical= */ true);
assertRequestFocus(mList5, false);
}
}));
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml
index d58cba2..02d7326 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml
@@ -41,7 +41,5 @@
<com.android.car.ui.recyclerview.CarUiRecyclerView
android:id="@+id/list"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:contentDescription="com.android.car.ui.utils.VERTICALLY_SCROLLABLE"/>
- <!-- TODO(b/171339427) Use app:rotaryScrollEnabled instead of contentDescription. -->
+ android:layout_height="wrap_content"/>
</LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml
index f5b9028..558dd9f 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml
@@ -91,8 +91,6 @@
<com.android.car.ui.recyclerview.CarUiRecyclerView
android:id="@+id/list5"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:contentDescription="com.android.car.ui.utils.VERTICALLY_SCROLLABLE"/>
- <!-- TODO(b/171339427) Use app:rotaryScrollEnabled instead of contentDescription. -->
+ android:layout_height="wrap_content"/>
</com.android.car.ui.FocusArea>
</LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
index fa2e65c..57e908f 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
@@ -16,6 +16,7 @@
package com.android.car.ui.recyclerview;
import static com.android.car.ui.utils.CarUiUtils.findViewByRefId;
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_CONTAINER;
import static com.android.car.ui.utils.RotaryConstants.ROTARY_HORIZONTALLY_SCROLLABLE;
import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
@@ -330,6 +331,11 @@
// Disable the default focus highlight. No highlight should appear when this view is
// focused.
setDefaultFocusHighlightEnabled(false);
+
+ // This view is a rotary container if it's not a scrollable container.
+ if (!rotaryScrollEnabled) {
+ super.setContentDescription(ROTARY_CONTAINER);
+ }
}
@Override
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java
index 1f1de0a..aaaae6c 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java
@@ -23,15 +23,34 @@
*/
public final class RotaryConstants {
/**
- * Content description indicating that the rotary controller should scroll this view
- * horizontally.
+ * Content description indicating that the view is a rotary container.
+ * <p>
+ * A rotary container contains focusable elements. When initializing focus, the first element
+ * in the rotary container is prioritized to take focus. When searching for nudge target, the
+ * bounds of the rotary container is the minimum bounds containing its descendants.
+ * <p>
+ * A rotary container shouldn't be focusable unless it's a scrollable container. Though it
+ * can't be focused, it can be scrolled as a side-effect of moving the focus within it.
+ */
+ public static final String ROTARY_CONTAINER =
+ "com.android.car.ui.utils.ROTARY_CONTAINER";
+
+ /**
+ * Content description indicating that the view is a scrollable container and can be scrolled
+ * horizontally by the rotary controller.
+ * <p>
+ * A scrollable container is a focusable rotary container. When it's focused, it can be scrolled
+ * when the rotary controller rotates. A scrollable container is often used to show long text.
*/
public static final String ROTARY_HORIZONTALLY_SCROLLABLE =
"com.android.car.ui.utils.HORIZONTALLY_SCROLLABLE";
/**
- * Content description indicating that the rotary controller should scroll this view
- * vertically.
+ * Content description indicating that the view is a scrollable container and can be scrolled
+ * vertically by the rotary controller.
+ * <p>
+ * A scrollable container is a focusable rotary container. When it's focused, it can be scrolled
+ * when the rotary controller rotates. A scrollable container is often used to show long text.
*/
public static final String ROTARY_VERTICALLY_SCROLLABLE =
"com.android.car.ui.utils.VERTICALLY_SCROLLABLE";
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
index 853649b..f1ffad6 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
@@ -18,6 +18,7 @@
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_CONTAINER;
import static com.android.car.ui.utils.RotaryConstants.ROTARY_HORIZONTALLY_SCROLLABLE;
import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
@@ -202,24 +203,31 @@
/**
* Returns whether the {@code view} is an implicit default focus view, i.e., the first focusable
- * item in a scrollable container.
+ * item in a rotary container.
*/
@VisibleForTesting
static boolean isImplicitDefaultFocusView(@NonNull View view) {
- ViewGroup scrollableContainer = null;
+ ViewGroup rotaryContainer = null;
ViewParent parent = view.getParent();
while (parent != null && parent instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) parent;
- if (isScrollableContainer(viewGroup)) {
- scrollableContainer = viewGroup;
+ if (isRotaryContainer(viewGroup)) {
+ rotaryContainer = viewGroup;
break;
}
parent = parent.getParent();
}
- if (scrollableContainer == null) {
+ if (rotaryContainer == null) {
return false;
}
- return findFirstFocusableDescendant(scrollableContainer) == view;
+ return findFirstFocusableDescendant(rotaryContainer) == view;
+ }
+
+ private static boolean isRotaryContainer(@NonNull View view) {
+ CharSequence contentDescription = view.getContentDescription();
+ return TextUtils.equals(contentDescription, ROTARY_CONTAINER)
+ || TextUtils.equals(contentDescription, ROTARY_VERTICALLY_SCROLLABLE)
+ || TextUtils.equals(contentDescription, ROTARY_HORIZONTALLY_SCROLLABLE);
}
private static boolean isScrollableContainer(@NonNull View view) {
@@ -317,16 +325,16 @@
/**
* Searches the {@code view} and its descendants in depth first order, and returns the first
- * implicit default focus view, i.e., the first focusable item in the first scrollable
- * container. Returns null if not found.
+ * implicit default focus view, i.e., the first focusable item in the first rotary container.
+ * Returns null if not found.
*/
@VisibleForTesting
@Nullable
static View findImplicitDefaultFocusView(@NonNull View view) {
- View scrollableContainer = findScrollableContainer(view);
- return scrollableContainer == null
+ View rotaryContainer = findRotaryContainer(view);
+ return rotaryContainer == null
? null
- : findFirstFocusableDescendant(scrollableContainer);
+ : findFirstFocusableDescendant(rotaryContainer);
}
/**
@@ -343,12 +351,12 @@
/**
* Searches the {@code view} and its descendants in depth first order, and returns the first
- * scrollable container shown on the screen. Returns null if not found.
+ * rotary container shown on the screen. Returns null if not found.
*/
@Nullable
- private static View findScrollableContainer(@NonNull View view) {
+ private static View findRotaryContainer(@NonNull View view) {
return depthFirstSearch(view,
- /* targetPredicate= */ v -> isScrollableContainer(v),
+ /* targetPredicate= */ v -> isRotaryContainer(v),
/* skipPredicate= */ v -> !v.isShown());
}