RecyclerView: dont clear app provided accessibility delegate
If app provides accessibility delegate in onCreateView, the delegate
is unexpectedly cleared when item is sent to RecyclerViewPool.
Test: notClearCustomViewDelegate clearItemDelegateWhenGoesToPool
Bug: 37672983
Change-Id: I688f94272956501daf8bd133acf6d15310335d6f
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index cb8c141..58641c8 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -5415,7 +5415,7 @@
mAdapter.bindViewHolder(holder, offsetPosition);
long endBindNs = getNanoTime();
mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
- attachAccessibilityDelegate(holder.itemView);
+ attachAccessibilityDelegateOnBind(holder);
if (mState.isPreLayout()) {
holder.mPreLayoutPosition = position;
}
@@ -5694,14 +5694,16 @@
return holder;
}
- private void attachAccessibilityDelegate(View itemView) {
+ private void attachAccessibilityDelegateOnBind(ViewHolder holder) {
if (isAccessibilityEnabled()) {
+ final View itemView = holder.itemView;
if (ViewCompat.getImportantForAccessibility(itemView)
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
ViewCompat.setImportantForAccessibility(itemView,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
if (!ViewCompat.hasAccessibilityDelegate(itemView)) {
+ holder.addFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(itemView,
mAccessibilityDelegate.getItemDelegate());
}
@@ -5902,7 +5904,10 @@
*/
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
- ViewCompat.setAccessibilityDelegate(holder.itemView, null);
+ if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
+ holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
+ ViewCompat.setAccessibilityDelegate(holder.itemView, null);
+ }
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
@@ -10352,6 +10357,12 @@
*/
static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13;
+ /**
+ * Flags that RecyclerView assigned {@link RecyclerViewAccessibilityDelegate
+ * #getItemDelegate()} in onBindView when app does not provide a delegate.
+ */
+ static final int FLAG_SET_A11Y_ITEM_DELEGATE = 1 << 14;
+
private int mFlags;
private static final List<Object> FULLUPDATE_PAYLOADS = Collections.EMPTY_LIST;
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityLifecycleTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityLifecycleTest.java
index bd14d2c..54bedb0 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityLifecycleTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityLifecycleTest.java
@@ -18,6 +18,11 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -25,12 +30,16 @@
import android.annotation.TargetApi;
import android.os.Build;
+import android.support.annotation.RequiresApi;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -150,4 +159,144 @@
}
}
}
+
+ @Test
+ @RequiresApi(14)
+ public void notClearCustomViewDelegate() throws Throwable {
+ final RecyclerView recyclerView = new RecyclerView(getActivity()) {
+ @Override
+ boolean isAccessibilityEnabled() {
+ return true;
+ }
+ };
+ final int[] layoutStart = new int[] {0};
+ final int layoutCount = 5;
+ final TestLayoutManager layoutManager = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ detachAndScrapAttachedViews(recycler);
+ removeAndRecycleScrapInt(recycler);
+ layoutRange(recycler, layoutStart[0], layoutStart[0] + layoutCount);
+ if (layoutLatch != null) {
+ layoutLatch.countDown();
+ }
+ }
+ };
+ final AccessibilityDelegateCompat delegateCompat = new AccessibilityDelegateCompat() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host,
+ AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setChecked(true);
+ }
+ };
+ final TestAdapter adapter = new TestAdapter(100) {
+ @Override
+ public TestViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
+ ViewCompat.setAccessibilityDelegate(vh.itemView, delegateCompat);
+ return vh;
+ }
+ };
+ layoutManager.expectLayouts(1);
+ recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 100);
+ recyclerView.setItemViewCacheSize(0); // no cache, directly goes to pool
+ recyclerView.setLayoutManager(layoutManager);
+ setRecyclerView(recyclerView);
+ recyclerView.setAdapter(adapter);
+ layoutManager.waitForLayout(1);
+
+ assertEquals(layoutCount, recyclerView.getChildCount());
+ ArrayList<View> children = new ArrayList();
+ for (int i = 0; i < recyclerView.getChildCount(); i++) {
+ View view = recyclerView.getChildAt(i);
+ assertEquals(layoutStart[0] + i, recyclerView.getChildAdapterPosition(view));
+ AccessibilityNodeInfo info = recyclerView.getChildAt(i).createAccessibilityNodeInfo();
+ assertTrue("custom delegate sets isChecked", info.isChecked());
+ assertFalse(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags(
+ RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
+ assertTrue(ViewCompat.hasAccessibilityDelegate(view));
+ children.add(view);
+ }
+
+ // invalidate and start layout at 50, all existing views will goes to recycler and
+ // being reused.
+ layoutStart[0] = 50;
+ layoutManager.expectLayouts(1);
+ adapter.dispatchDataSetChanged();
+ layoutManager.waitForLayout(1);
+ assertEquals(layoutCount, recyclerView.getChildCount());
+ for (int i = 0; i < recyclerView.getChildCount(); i++) {
+ View view = recyclerView.getChildAt(i);
+ assertEquals(layoutStart[0] + i, recyclerView.getChildAdapterPosition(view));
+ assertTrue(children.contains(view));
+ AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
+ assertTrue("custom delegate sets isChecked", info.isChecked());
+ assertFalse(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags(
+ RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
+ assertTrue(ViewCompat.hasAccessibilityDelegate(view));
+ }
+ }
+
+ @Test
+ @RequiresApi(14)
+ public void clearItemDelegateWhenGoesToPool() throws Throwable {
+ final RecyclerView recyclerView = new RecyclerView(getActivity()) {
+ @Override
+ boolean isAccessibilityEnabled() {
+ return true;
+ }
+ };
+ final int firstPassLayoutCount = 5;
+ final int[] layoutCount = new int[] {firstPassLayoutCount};
+ final TestLayoutManager layoutManager = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ detachAndScrapAttachedViews(recycler);
+ removeAndRecycleScrapInt(recycler);
+ layoutRange(recycler, 0, layoutCount[0]);
+ if (layoutLatch != null) {
+ layoutLatch.countDown();
+ }
+ }
+ };
+ final TestAdapter adapter = new TestAdapter(100);
+ layoutManager.expectLayouts(1);
+ recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 100);
+ recyclerView.setItemViewCacheSize(0); // no cache, directly goes to pool
+ recyclerView.setLayoutManager(layoutManager);
+ setRecyclerView(recyclerView);
+ recyclerView.setAdapter(adapter);
+ layoutManager.waitForLayout(1);
+
+ assertEquals(firstPassLayoutCount, recyclerView.getChildCount());
+ for (int i = 0; i < recyclerView.getChildCount(); i++) {
+ View view = recyclerView.getChildAt(i);
+ assertEquals(i, recyclerView.getChildAdapterPosition(view));
+ assertTrue(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags(
+ RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
+ assertTrue(ViewCompat.hasAccessibilityDelegate(view));
+ AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
+ assertNotNull(info.getCollectionItemInfo());
+ }
+
+ // let all items go to recycler pool
+ layoutManager.expectLayouts(1);
+ layoutCount[0] = 0;
+ adapter.resetItemsTo(new ArrayList());
+ layoutManager.waitForLayout(1);
+ assertEquals(0, recyclerView.getChildCount());
+ assertEquals(firstPassLayoutCount, recyclerView.getRecycledViewPool()
+ .getRecycledViewCount(0));
+ for (int i = 0; i < firstPassLayoutCount; i++) {
+ RecyclerView.ViewHolder vh = recyclerView.getRecycledViewPool().getRecycledView(0);
+ View view = vh.itemView;
+ assertEquals(RecyclerView.NO_POSITION, recyclerView.getChildAdapterPosition(view));
+ assertFalse(vh.hasAnyOfTheFlags(RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE));
+ assertFalse(ViewCompat.hasAccessibilityDelegate(view));
+ AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
+ assertNull(info.getCollectionItemInfo());
+ }
+ }
}