Prevent RecyclerView from doing another layout while processing updates
This CL fixes a bug in RecyclerView where it would try to consume
pending udpates twice if it receives a call to layout (focus search etc)
while processing updates.
Bug: 32316425
Test: RecyclerViewLayoutTest::triggerFocusSearchInOnRecycledCallback
Change-Id: I3dccc7fb8ef5fffc260220254a1b6605d88b6e77
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 7bb8a3f..e159ac6 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -1577,6 +1577,7 @@
.hasAnyUpdateTypes(UpdateOp.ADD | UpdateOp.REMOVE | UpdateOp.MOVE)) {
TraceCompat.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG);
eatRequestLayout();
+ onEnterLayoutOrScroll();
mAdapterHelper.preProcess();
if (!mLayoutRequestEaten) {
if (hasUpdatedView()) {
@@ -1587,6 +1588,7 @@
}
}
resumeRequestLayout(true);
+ onExitLayoutOrScroll();
TraceCompat.endSection();
} else if (mAdapterHelper.hasPendingUpdates()) {
TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 770ab71..80adf64 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -97,6 +97,64 @@
}
@Test
+ public void triggerFocusSearchInOnRecycledCallback() throws Throwable {
+ final RecyclerView rv = new RecyclerView(getActivity()) {
+ @Override
+ void consumePendingUpdateOperations() {
+ try {
+ super.consumePendingUpdateOperations();
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ }
+ }
+ };
+ final AtomicBoolean receivedOnRecycled = new AtomicBoolean(false);
+ final TestAdapter adapter = new TestAdapter(20) {
+ @Override
+ public void onViewRecycled(TestViewHolder holder) {
+ super.onViewRecycled(holder);
+ if (receivedOnRecycled.getAndSet(true)) {
+ return;
+ }
+ rv.focusSearch(rv.getChildAt(0), View.FOCUS_FORWARD);
+ }
+ };
+ final AtomicInteger layoutCnt = new AtomicInteger(5);
+ TestLayoutManager tlm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ detachAndScrapAttachedViews(recycler);
+ layoutRange(recycler, 0, layoutCnt.get());
+ layoutLatch.countDown();
+ }
+ };
+ rv.setLayoutManager(tlm);
+ rv.setAdapter(adapter);
+ tlm.expectLayouts(1);
+ setRecyclerView(rv);
+ tlm.waitForLayout(2);
+
+ layoutCnt.set(4);
+ tlm.expectLayouts(1);
+ requestLayoutOnUIThread(rv);
+ tlm.waitForLayout(1);
+
+ assertThat("test sanity", rv.mRecycler.mCachedViews.size(), is(1));
+ tlm.expectLayouts(1);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ adapter.notifyItemChanged(4);
+ rv.smoothScrollBy(0, 1);
+ }
+ });
+ checkForMainThreadException();
+ tlm.waitForLayout(2);
+ assertThat("test sanity", rv.mRecycler.mCachedViews.size(), is(0));
+ assertThat(receivedOnRecycled.get(), is(true));
+ }
+
+ @Test
public void detachAttachGetReadyWithoutChanges() throws Throwable {
detachAttachGetReady(false, false, false);
}