/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v7.widget; import static android.support.v7.widget.RecyclerView.NO_POSITION; import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING; import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE; import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING; import static android.support.v7.widget.RecyclerView.getChildViewHolderInt; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.sameInstance; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.Color; import android.graphics.PointF; import android.graphics.Rect; import android.os.Build; import android.os.SystemClock; import android.support.annotation.Nullable; import android.support.test.filters.FlakyTest; import android.support.test.filters.LargeTest; import android.support.test.filters.MediumTest; import android.support.test.filters.SdkSuppress; import android.support.test.filters.Suppress; import android.support.test.runner.AndroidJUnit4; import android.support.v4.view.ViewCompat; import android.support.v7.util.TouchUtils; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.LinearLayout; import android.widget.TextView; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @RunWith(AndroidJUnit4.class) @MediumTest public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest { private static final int FLAG_HORIZONTAL = 1; private static final int FLAG_VERTICAL = 1 << 1; private static final int FLAG_FLING = 1 << 2; private static final boolean DEBUG = false; private static final String TAG = "RecyclerViewLayoutTest"; public RecyclerViewLayoutTest() { super(DEBUG); } @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); mActivityRule.runOnUiThread(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); } @Test public void detachAttachGetReadyRequireLayout() throws Throwable { detachAttachGetReady(true, false, false); } @Test public void detachAttachGetReadyRemoveAdapter() throws Throwable { detachAttachGetReady(false, true, false); } @Test public void detachAttachGetReadyRemoveLayoutManager() throws Throwable { detachAttachGetReady(false, false, true); } private void detachAttachGetReady(final boolean requestLayoutOnDetach, final boolean removeAdapter, final boolean removeLayoutManager) throws Throwable { final LinearLayout ll1 = new LinearLayout(getActivity()); final LinearLayout ll2 = new LinearLayout(getActivity()); final LinearLayout ll3 = new LinearLayout(getActivity()); final RecyclerView rv = new RecyclerView(getActivity()); ll1.addView(ll2); ll2.addView(ll3); ll3.addView(rv); TestLayoutManager layoutManager = new TestLayoutManager() { @Override public void onLayoutCompleted(RecyclerView.State state) { super.onLayoutCompleted(state); layoutLatch.countDown(); } @Override public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { super.onDetachedFromWindow(view, recycler); if (requestLayoutOnDetach) { view.requestLayout(); } } }; rv.setLayoutManager(layoutManager); rv.setAdapter(new TestAdapter(10)); layoutManager.expectLayouts(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { getActivity().getContainer().addView(ll1); } }); layoutManager.waitForLayout(2); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { ll1.removeView(ll2); } }); getInstrumentation().waitForIdleSync(); if (removeLayoutManager) { rv.setLayoutManager(null); rv.setLayoutManager(layoutManager); } if (removeAdapter) { rv.setAdapter(null); rv.setAdapter(new TestAdapter(10)); } final boolean requireLayout = requestLayoutOnDetach || removeAdapter || removeLayoutManager; layoutManager.expectLayouts(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { ll1.addView(ll2); if (requireLayout) { assertTrue(rv.hasPendingAdapterUpdates()); assertFalse(rv.mFirstLayoutComplete); } else { assertFalse(rv.hasPendingAdapterUpdates()); assertTrue(rv.mFirstLayoutComplete); } } }); if (requireLayout) { layoutManager.waitForLayout(2); } else { layoutManager.assertNoLayout("nothing is invalid, layout should not happen", 2); } } @Test public void detachRvAndLayoutManagerProperly() throws Throwable { final RecyclerView rv = new RecyclerView(getActivity()); final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true) { @Override public void onAttachedToWindow(RecyclerView view) { super.onAttachedToWindow(view); assertThat(view.mLayout, is((RecyclerView.LayoutManager) this)); } @Override public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { super.onDetachedFromWindow(view, recycler); assertThat(view.mLayout, is((RecyclerView.LayoutManager) this)); } }; final Runnable check = new Runnable() { @Override public void run() { assertThat("bound between the RV and the LM should be disconnected at the" + " same time", rv.mLayout == lm, is(lm.mRecyclerView == rv)); } }; final AtomicInteger detachCounter = new AtomicInteger(0); rv.setAdapter(new TestAdapter(10) { @Override public void onBindViewHolder(TestViewHolder holder, int position) { super.onBindViewHolder(holder, position); holder.itemView.setFocusable(true); holder.itemView.setFocusableInTouchMode(true); } @Override public void onViewDetachedFromWindow(TestViewHolder holder) { super.onViewDetachedFromWindow(holder); detachCounter.incrementAndGet(); check.run(); } @Override public void onViewRecycled(TestViewHolder holder) { super.onViewRecycled(holder); check.run(); } }); rv.setLayoutManager(lm); lm.expectLayouts(1); setRecyclerView(rv); lm.waitForLayout(2); assertThat("test sanity", rv.getChildCount(), is(10)); final TestLayoutManager replacement = new LayoutAllLayoutManager(true); replacement.expectLayouts(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { rv.setLayoutManager(replacement); } }); replacement.waitForLayout(2); assertThat("test sanity", rv.getChildCount(), is(10)); assertThat("all initial views should be detached", detachCounter.get(), is(10)); checkForMainThreadException(); } @Test public void focusSearchWithOtherFocusables() throws Throwable { final LinearLayout container = new LinearLayout(getActivity()); container.setOrientation(LinearLayout.VERTICAL); RecyclerView rv = new RecyclerView(getActivity()); mRecyclerView = rv; rv.setAdapter(new TestAdapter(10) { @Override public void onBindViewHolder(TestViewHolder holder, int position) { super.onBindViewHolder(holder, position); holder.itemView.setFocusableInTouchMode(true); holder.itemView.setLayoutParams( new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } }); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, 1); layoutLatch.countDown(); } @Nullable @Override public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state) { assertEquals(View.FOCUS_FORWARD, direction); assertEquals(1, getChildCount()); View child0 = getChildAt(0); View view = recycler.getViewForPosition(1); addView(view); measureChild(view, 0, 0); layoutDecorated(view, 0, child0.getBottom(), getDecoratedMeasuredWidth(view), child0.getBottom() + getDecoratedMeasuredHeight(view)); return view; } @Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { super.scrollHorizontallyBy(dx, recycler, state); // offset by -dx because the views translate opposite of the scrolling direction mRecyclerView.offsetChildrenHorizontal(-dx); return dx; } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { super.scrollVerticallyBy(dy, recycler, state); // offset by -dy because the views translate opposite of the scrolling direction mRecyclerView.offsetChildrenVertical(-dy); return dy; } }; tlm.setAutoMeasureEnabled(true); rv.setLayoutManager(tlm); TextView viewAbove = new TextView(getActivity()); viewAbove.setText("view above"); viewAbove.setFocusableInTouchMode(true); container.addView(viewAbove); container.addView(rv); TextView viewBelow = new TextView(getActivity()); viewBelow.setText("view below"); viewBelow.setFocusableInTouchMode(true); container.addView(viewBelow); tlm.expectLayouts(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { getActivity().getContainer().addView(container); } }); tlm.waitForLayout(2); requestFocus(viewAbove, true); assertTrue(viewAbove.hasFocus()); View newFocused = focusSearch(viewAbove, View.FOCUS_FORWARD); assertThat(newFocused, sameInstance(rv.getChildAt(0))); newFocused = focusSearch(rv.getChildAt(0), View.FOCUS_FORWARD); assertThat(newFocused, sameInstance(rv.getChildAt(1))); } @Test public void boundingBoxNoTranslation() throws Throwable { transformedBoundingBoxTest(new ViewRunnable() { @Override public void run(View view) throws RuntimeException { view.layout(10, 10, 30, 50); assertThat(getTransformedBoundingBox(view), is(new Rect(10, 10, 30, 50))); } }); } @Test public void boundingBoxTranslateX() throws Throwable { transformedBoundingBoxTest(new ViewRunnable() { @Override public void run(View view) throws RuntimeException { view.layout(10, 10, 30, 50); ViewCompat.setTranslationX(view, 10); assertThat(getTransformedBoundingBox(view), is(new Rect(20, 10, 40, 50))); } }); } @Test public void boundingBoxTranslateY() throws Throwable { transformedBoundingBoxTest(new ViewRunnable() { @Override public void run(View view) throws RuntimeException { view.layout(10, 10, 30, 50); ViewCompat.setTranslationY(view, 10); assertThat(getTransformedBoundingBox(view), is(new Rect(10, 20, 30, 60))); } }); } @Test public void boundingBoxScaleX() throws Throwable { transformedBoundingBoxTest(new ViewRunnable() { @Override public void run(View view) throws RuntimeException { view.layout(10, 10, 30, 50); ViewCompat.setScaleX(view, 2); assertThat(getTransformedBoundingBox(view), is(new Rect(0, 10, 40, 50))); } }); } @Test public void boundingBoxScaleY() throws Throwable { transformedBoundingBoxTest(new ViewRunnable() { @Override public void run(View view) throws RuntimeException { view.layout(10, 10, 30, 50); ViewCompat.setScaleY(view, 2); assertThat(getTransformedBoundingBox(view), is(new Rect(10, -10, 30, 70))); } }); } @Test public void boundingBoxRotated() throws Throwable { transformedBoundingBoxTest(new ViewRunnable() { @Override public void run(View view) throws RuntimeException { view.layout(10, 10, 30, 50); ViewCompat.setRotation(view, 90); assertThat(getTransformedBoundingBox(view), is(new Rect(0, 20, 40, 40))); } }); } @Test public void boundingBoxRotatedWithDecorOffsets() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final TestAdapter adapter = new TestAdapter(1); recyclerView.setAdapter(adapter); recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { outRect.set(1, 2, 3, 4); } }); TestLayoutManager layoutManager = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); View view = recycler.getViewForPosition(0); addView(view); view.measure( View.MeasureSpec.makeMeasureSpec(20, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(40, View.MeasureSpec.EXACTLY) ); // trigger decor offsets calculation calculateItemDecorationsForChild(view, new Rect()); view.layout(10, 10, 30, 50); ViewCompat.setRotation(view, 90); assertThat(RecyclerViewLayoutTest.this.getTransformedBoundingBox(view), is(new Rect(-4, 19, 42, 43))); layoutLatch.countDown(); } }; recyclerView.setLayoutManager(layoutManager); layoutManager.expectLayouts(1); setRecyclerView(recyclerView); layoutManager.waitForLayout(2); checkForMainThreadException(); } private Rect getTransformedBoundingBox(View child) { Rect rect = new Rect(); mRecyclerView.getLayoutManager().getTransformedBoundingBox(child, true, rect); return rect; } public void transformedBoundingBoxTest(final ViewRunnable layout) throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final TestAdapter adapter = new TestAdapter(1); recyclerView.setAdapter(adapter); TestLayoutManager layoutManager = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); View view = recycler.getViewForPosition(0); addView(view); view.measure( View.MeasureSpec.makeMeasureSpec(20, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(40, View.MeasureSpec.EXACTLY) ); layout.run(view); layoutLatch.countDown(); } }; recyclerView.setLayoutManager(layoutManager); layoutManager.expectLayouts(1); setRecyclerView(recyclerView); layoutManager.waitForLayout(2); checkForMainThreadException(); } @Test public void flingFrozen() throws Throwable { testScrollFrozen(true); } @Test public void dragFrozen() throws Throwable { testScrollFrozen(false); } @Test public void requestRectOnScreenWithScrollOffset() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final LayoutAllLayoutManager tlm = spy(new LayoutAllLayoutManager()); final int scrollY = 50; RecyclerView.Adapter adapter = new RecyclerView.Adapter() { @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = new View(parent.getContext()); view.setScrollY(scrollY); return new RecyclerView.ViewHolder(view) { }; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {} @Override public int getItemCount() { return 1; } }; recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); final View child = recyclerView.getChildAt(0); assertThat(child.getScrollY(), CoreMatchers.is(scrollY)); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { recyclerView.requestChildRectangleOnScreen(child, new Rect(3, 4, 5, 6), true); verify(tlm, times(1)).scrollVerticallyBy(eq(-46), any(RecyclerView.Recycler.class), any(RecyclerView.State.class)); } }); } @Test public void reattachAndScrollCrash() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final TestLayoutManager tlm = new TestLayoutManager() { @Override public boolean canScrollVertically() { return true; } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutRange(recycler, 0, Math.min(state.getItemCount(), 10)); } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { // Access views in the state (that might have been deleted). for (int i = 10; i < state.getItemCount(); i++) { recycler.getViewForPosition(i); } return dy; } }; final TestAdapter adapter = new TestAdapter(12); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(tlm); setRecyclerView(recyclerView); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { getActivity().getContainer().removeView(recyclerView); getActivity().getContainer().addView(recyclerView); try { adapter.deleteAndNotify(1, adapter.getItemCount() - 1); } catch (Throwable throwable) { postExceptionToInstrumentation(throwable); } recyclerView.scrollBy(0, 10); } }); } private void testScrollFrozen(boolean fling) throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); final int horizontalScrollCount = 3; final int verticalScrollCount = 3; final int horizontalVelocity = 1000; final int verticalVelocity = 1000; final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount); final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount); TestLayoutManager tlm = new TestLayoutManager() { @Override public boolean canScrollHorizontally() { return true; } @Override public boolean canScrollVertically() { return true; } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutRange(recycler, 0, 10); layoutLatch.countDown(); } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (verticalCounter.get() > 0) { verticalCounter.decrementAndGet(); return dy; } return 0; } @Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { if (horizontalCounter.get() > 0) { horizontalCounter.decrementAndGet(); return dx; } return 0; } }; TestAdapter adapter = new TestAdapter(100); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(2); freezeLayout(true); if (fling) { assertFalse("fling should be blocked", fling(horizontalVelocity, verticalVelocity)); } else { // drag TouchUtils.dragViewTo(getInstrumentation(), recyclerView, Gravity.LEFT | Gravity.TOP, mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); } assertEquals("rv's horizontal scroll cb must not run", horizontalScrollCount, horizontalCounter.get()); assertEquals("rv's vertical scroll cb must not run", verticalScrollCount, verticalCounter.get()); freezeLayout(false); if (fling) { assertTrue("fling should be started", fling(horizontalVelocity, verticalVelocity)); } else { // drag TouchUtils.dragViewTo(getInstrumentation(), recyclerView, Gravity.LEFT | Gravity.TOP, mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); } assertEquals("rv's horizontal scroll cb must finishes", 0, horizontalCounter.get()); assertEquals("rv's vertical scroll cb must finishes", 0, verticalCounter.get()); } @Test public void testFocusSearchAfterChangedData() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutRange(recycler, 0, 2); layoutLatch.countDown(); } @Nullable @Override public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state) { try { recycler.getViewForPosition(state.getItemCount() - 1); } catch (Throwable t) { postExceptionToInstrumentation(t); } return null; } }; recyclerView.setLayoutManager(tlm); final TestAdapter adapter = new TestAdapter(10) { @Override public void onBindViewHolder(TestViewHolder holder, int position) { super.onBindViewHolder(holder, position); holder.itemView.setFocusable(false); holder.itemView.setFocusableInTouchMode(false); } }; recyclerView.setAdapter(adapter); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { adapter.mItems.remove(9); adapter.notifyItemRemoved(9); recyclerView.focusSearch(recyclerView.getChildAt(1), View.FOCUS_DOWN); } }); checkForMainThreadException(); } @Test public void testFocusSearchWithRemovedFocusedItem() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setItemAnimator(null); TestLayoutManager tlm = new LayoutAllLayoutManager(); recyclerView.setLayoutManager(tlm); final TestAdapter adapter = new TestAdapter(10) { @Override public void onBindViewHolder(TestViewHolder holder, int position) { super.onBindViewHolder(holder, position); holder.itemView.setFocusable(true); holder.itemView.setFocusableInTouchMode(true); } }; recyclerView.setAdapter(adapter); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); final RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(9); requestFocus(toFocus.itemView, true); assertThat("test sanity", toFocus.itemView.hasFocus(), is(true)); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { adapter.mItems.remove(9); adapter.notifyItemRemoved(9); recyclerView.focusSearch(toFocus.itemView, View.FOCUS_DOWN); } }); checkForMainThreadException(); } @Test public void testFocusSearchFailFrozen() throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); final CountDownLatch focusLatch = new CountDownLatch(1); final AtomicInteger focusSearchCalled = new AtomicInteger(0); TestLayoutManager tlm = new TestLayoutManager() { @Override public boolean canScrollHorizontally() { return true; } @Override public boolean canScrollVertically() { return true; } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutRange(recycler, 0, 10); layoutLatch.countDown(); } @Override public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state) { focusSearchCalled.addAndGet(1); focusLatch.countDown(); return null; } }; TestAdapter adapter = new TestAdapter(100); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(2); final View c = recyclerView.getChildAt(recyclerView.getChildCount() - 1); assertTrue("test sanity", requestFocus(c, true)); assertTrue("test sanity", c.hasFocus()); freezeLayout(true); focusSearch(recyclerView, c, View.FOCUS_DOWN); assertEquals("onFocusSearchFailed should not be called when layout is frozen", 0, focusSearchCalled.get()); freezeLayout(false); focusSearch(c, View.FOCUS_DOWN); assertTrue(focusLatch.await(2, TimeUnit.SECONDS)); assertEquals(1, focusSearchCalled.get()); } public View focusSearch(final ViewGroup parent, final View focused, final int direction) throws Throwable { final View[] result = new View[1]; mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { result[0] = parent.focusSearch(focused, direction); } }); return result[0]; } @Test public void frozenAndChangeAdapter() throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); final AtomicInteger focusSearchCalled = new AtomicInteger(0); TestLayoutManager tlm = new TestLayoutManager() { @Override public boolean canScrollHorizontally() { return true; } @Override public boolean canScrollVertically() { return true; } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutRange(recycler, 0, 10); layoutLatch.countDown(); } @Override public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state) { focusSearchCalled.addAndGet(1); return null; } }; TestAdapter adapter = new TestAdapter(100); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(2); freezeLayout(true); TestAdapter adapter2 = new TestAdapter(1000); setAdapter(adapter2); assertFalse(recyclerView.isLayoutFrozen()); assertSame(adapter2, recyclerView.getAdapter()); freezeLayout(true); TestAdapter adapter3 = new TestAdapter(1000); swapAdapter(adapter3, true); assertFalse(recyclerView.isLayoutFrozen()); assertSame(adapter3, recyclerView.getAdapter()); } @Test public void noLayoutIf0ItemsAreChanged() throws Throwable { unnecessaryNotifyEvents(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.notifyItemRangeChanged(3, 0); } }); } @Test public void noLayoutIf0ItemsAreChangedWithPayload() throws Throwable { unnecessaryNotifyEvents(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.notifyItemRangeChanged(0, 0, new Object()); } }); } @Test public void noLayoutIf0ItemsAreAdded() throws Throwable { unnecessaryNotifyEvents(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.notifyItemRangeInserted(3, 0); } }); } @Test public void noLayoutIf0ItemsAreRemoved() throws Throwable { unnecessaryNotifyEvents(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.notifyItemRangeRemoved(3, 0); } }); } @Test public void noLayoutIfItemMovedIntoItsOwnPlace() throws Throwable { unnecessaryNotifyEvents(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.notifyItemMoved(3, 3); } }); } public void unnecessaryNotifyEvents(final AdapterRunnable action) throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final TestAdapter adapter = new TestAdapter(5); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); layoutLatch.countDown(); } }; recyclerView.setLayoutManager(tlm); recyclerView.setAdapter(adapter); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); // ready tlm.expectLayouts(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { try { action.run(adapter); } catch (Throwable throwable) { postExceptionToInstrumentation(throwable); } } }); tlm.assertNoLayout("dummy event should not trigger a layout", 1); checkForMainThreadException(); } @Test public void scrollToPositionCallback() throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); TestLayoutManager tlm = new TestLayoutManager() { int scrollPos = RecyclerView.NO_POSITION; @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutLatch.countDown(); if (scrollPos == RecyclerView.NO_POSITION) { layoutRange(recycler, 0, 10); } else { layoutRange(recycler, scrollPos, scrollPos + 10); } } @Override public void scrollToPosition(int position) { scrollPos = position; requestLayout(); } }; recyclerView.setLayoutManager(tlm); TestAdapter adapter = new TestAdapter(100); recyclerView.setAdapter(adapter); final AtomicInteger rvCounter = new AtomicInteger(0); final AtomicInteger viewGroupCounter = new AtomicInteger(0); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { rvCounter.incrementAndGet(); super.onScrolled(recyclerView, dx, dy); } }); getRecyclerViewContainer().getViewTreeObserver().addOnScrollChangedListener( new ViewTreeObserver.OnScrollChangedListener() { @Override public void onScrollChanged() { viewGroupCounter.incrementAndGet(); } }); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(2); assertEquals("RV on scroll should be called for initialization", 1, rvCounter.get()); assertEquals("VTO on scroll should be called for initialization", 1, viewGroupCounter.get()); tlm.expectLayouts(1); freezeLayout(true); scrollToPosition(3); tlm.assertNoLayout("scrollToPosition should be ignored", 2); freezeLayout(false); scrollToPosition(3); tlm.waitForLayout(2); assertEquals("RV on scroll should be called", 2, rvCounter.get()); assertEquals("VTO on scroll should be called", 2, viewGroupCounter.get()); tlm.expectLayouts(1); requestLayoutOnUIThread(recyclerView); tlm.waitForLayout(2); // wait for draw :/ Thread.sleep(1000); assertEquals("on scroll should NOT be called", 2, rvCounter.get()); assertEquals("on scroll should NOT be called", 2, viewGroupCounter.get()); } @Test public void scrollCallbackFromEmptyToSome() throws Throwable { scrollCallbackOnVisibleRangeChange(1, new int[]{0, 0}, new int[]{0, 1}); } @Test public void scrollCallbackOnVisibleRangeExpand() throws Throwable { scrollCallbackOnVisibleRangeChange(10, new int[]{3, 5}, new int[]{3, 6}); } @Test public void scrollCallbackOnVisibleRangeShrink() throws Throwable { scrollCallbackOnVisibleRangeChange(10, new int[]{3, 6}, new int[]{3, 5}); } @Test public void scrollCallbackOnVisibleRangeExpand2() throws Throwable { scrollCallbackOnVisibleRangeChange(10, new int[]{3, 5}, new int[]{2, 5}); } @Test public void scrollCallbackOnVisibleRangeShrink2() throws Throwable { scrollCallbackOnVisibleRangeChange(10, new int[]{3, 6}, new int[]{2, 6}); } private void scrollCallbackOnVisibleRangeChange(int itemCount, final int[] beforeRange, final int[] afterRange) throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()) { @Override void dispatchLayout() { super.dispatchLayout(); ((TestLayoutManager) getLayoutManager()).layoutLatch.countDown(); } }; final AtomicBoolean beforeState = new AtomicBoolean(true); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); int[] range = beforeState.get() ? beforeRange : afterRange; layoutRange(recycler, range[0], range[1]); } }; recyclerView.setLayoutManager(tlm); final TestAdapter adapter = new TestAdapter(itemCount); recyclerView.setAdapter(adapter); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); RecyclerView.OnScrollListener mockListener = mock(RecyclerView.OnScrollListener.class); recyclerView.addOnScrollListener(mockListener); verify(mockListener, never()).onScrolled(any(RecyclerView.class), anyInt(), anyInt()); tlm.expectLayouts(1); beforeState.set(false); requestLayoutOnUIThread(recyclerView); tlm.waitForLayout(2); checkForMainThreadException(); verify(mockListener).onScrolled(recyclerView, 0, 0); } @Test public void addItemOnScroll() throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); final AtomicInteger start = new AtomicInteger(0); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutRange(recycler, start.get(), start.get() + 10); layoutLatch.countDown(); } }; recyclerView.setLayoutManager(tlm); final TestAdapter adapter = new TestAdapter(100); recyclerView.setAdapter(adapter); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); final Throwable[] error = new Throwable[1]; final AtomicBoolean calledOnScroll = new AtomicBoolean(false); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); calledOnScroll.set(true); try { adapter.addAndNotify(5, 20); } catch (Throwable throwable) { error[0] = throwable; } } }); start.set(4); MatcherAssert.assertThat("test sanity", calledOnScroll.get(), CoreMatchers.is(false)); tlm.expectLayouts(1); requestLayoutOnUIThread(recyclerView); tlm.waitForLayout(2); checkForMainThreadException(); MatcherAssert.assertThat("test sanity", calledOnScroll.get(), CoreMatchers.is(true)); MatcherAssert.assertThat(error[0], CoreMatchers.nullValue()); } @Test public void scrollInBothDirectionEqual() throws Throwable { scrollInBothDirection(3, 3, 1000, 1000); } @Test public void scrollInBothDirectionMoreVertical() throws Throwable { scrollInBothDirection(2, 3, 1000, 1000); } @Test public void scrollInBothDirectionMoreHorizontal() throws Throwable { scrollInBothDirection(3, 2, 1000, 1000); } @Test public void scrollHorizontalOnly() throws Throwable { scrollInBothDirection(3, 0, 1000, 0); } @Test public void scrollVerticalOnly() throws Throwable { scrollInBothDirection(0, 3, 0, 1000); } @Test public void scrollInBothDirectionEqualReverse() throws Throwable { scrollInBothDirection(3, 3, -1000, -1000); } @Test public void scrollInBothDirectionMoreVerticalReverse() throws Throwable { scrollInBothDirection(2, 3, -1000, -1000); } @Test public void scrollInBothDirectionMoreHorizontalReverse() throws Throwable { scrollInBothDirection(3, 2, -1000, -1000); } @Test public void scrollHorizontalOnlyReverse() throws Throwable { scrollInBothDirection(3, 0, -1000, 0); } @Test public void scrollVerticalOnlyReverse() throws Throwable { scrollInBothDirection(0, 3, 0, -1000); } public void scrollInBothDirection(int horizontalScrollCount, int verticalScrollCount, int horizontalVelocity, int verticalVelocity) throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount); final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount); TestLayoutManager tlm = new TestLayoutManager() { @Override public boolean canScrollHorizontally() { return true; } @Override public boolean canScrollVertically() { return true; } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutRange(recycler, 0, 10); layoutLatch.countDown(); } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (verticalCounter.get() > 0) { verticalCounter.decrementAndGet(); return dy; } return 0; } @Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { if (horizontalCounter.get() > 0) { horizontalCounter.decrementAndGet(); return dx; } return 0; } }; TestAdapter adapter = new TestAdapter(100); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(2); assertTrue("test sanity, fling must run", fling(horizontalVelocity, verticalVelocity)); assertEquals("rv's horizontal scroll cb must run " + horizontalScrollCount + " times'", 0, horizontalCounter.get()); assertEquals("rv's vertical scroll cb must run " + verticalScrollCount + " times'", 0, verticalCounter.get()); } @Test public void dragHorizontal() throws Throwable { scrollInOtherOrientationTest(FLAG_HORIZONTAL); } @Test public void dragVertical() throws Throwable { scrollInOtherOrientationTest(FLAG_VERTICAL); } @Test public void flingHorizontal() throws Throwable { scrollInOtherOrientationTest(FLAG_HORIZONTAL | FLAG_FLING); } @Test public void flingVertical() throws Throwable { scrollInOtherOrientationTest(FLAG_VERTICAL | FLAG_FLING); } @Test public void nestedDragVertical() throws Throwable { TestedFrameLayout tfl = getActivity().getContainer(); tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); scrollInOtherOrientationTest(FLAG_VERTICAL, 0); } @Test public void nestedDragHorizontal() throws Throwable { TestedFrameLayout tfl = getActivity().getContainer(); tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0); } @Test public void nestedDragHorizontalCallsStopNestedScroll() throws Throwable { TestedFrameLayout tfl = getActivity().getContainer(); tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0); assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled()); } @Test public void nestedDragVerticalCallsStopNestedScroll() throws Throwable { TestedFrameLayout tfl = getActivity().getContainer(); tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); scrollInOtherOrientationTest(FLAG_VERTICAL, 0); assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled()); } private void scrollInOtherOrientationTest(int flags) throws Throwable { scrollInOtherOrientationTest(flags, flags); } private void scrollInOtherOrientationTest(final int flags, int expectedFlags) throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); final AtomicBoolean scrolledHorizontal = new AtomicBoolean(false); final AtomicBoolean scrolledVertical = new AtomicBoolean(false); final TestLayoutManager tlm = new TestLayoutManager() { @Override public boolean canScrollHorizontally() { return (flags & FLAG_HORIZONTAL) != 0; } @Override public boolean canScrollVertically() { return (flags & FLAG_VERTICAL) != 0; } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutRange(recycler, 0, 10); layoutLatch.countDown(); } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { scrolledVertical.set(true); return super.scrollVerticallyBy(dy, recycler, state); } @Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { scrolledHorizontal.set(true); return super.scrollHorizontallyBy(dx, recycler, state); } }; TestAdapter adapter = new TestAdapter(100); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(2); if ( (flags & FLAG_FLING) != 0 ) { int flingVelocity = (mRecyclerView.getMaxFlingVelocity() + mRecyclerView.getMinFlingVelocity()) / 2; assertEquals("fling started", (expectedFlags & FLAG_FLING) != 0, fling(flingVelocity, flingVelocity)); } else { // drag TouchUtils.dragViewTo(getInstrumentation(), recyclerView, Gravity.LEFT | Gravity.TOP, mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); } assertEquals("horizontally scrolled: " + tlm.mScrollHorizontallyAmount, (expectedFlags & FLAG_HORIZONTAL) != 0, scrolledHorizontal.get()); assertEquals("vertically scrolled: " + tlm.mScrollVerticallyAmount, (expectedFlags & FLAG_VERTICAL) != 0, scrolledVertical.get()); } private boolean fling(final int velocityX, final int velocityY) throws Throwable { final AtomicBoolean didStart = new AtomicBoolean(false); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { boolean result = mRecyclerView.fling(velocityX, velocityY); didStart.set(result); } }); if (!didStart.get()) { return false; } waitForIdleScroll(mRecyclerView); return true; } private void assertPendingUpdatesAndLayoutTest(final AdapterRunnable runnable) throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); TestLayoutManager layoutManager = new DumbLayoutManager(); final TestAdapter testAdapter = new TestAdapter(10); setupBasic(recyclerView, layoutManager, testAdapter, false); layoutManager.expectLayouts(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { try { runnable.run(testAdapter); } catch (Throwable throwable) { fail("runnable has thrown an exception"); } assertTrue(mRecyclerView.hasPendingAdapterUpdates()); } }); layoutManager.waitForLayout(1); assertFalse(mRecyclerView.hasPendingAdapterUpdates()); checkForMainThreadException(); } private void setupBasic(RecyclerView recyclerView, TestLayoutManager tlm, TestAdapter adapter, boolean waitForFirstLayout) throws Throwable { recyclerView.setLayoutManager(tlm); recyclerView.setAdapter(adapter); if (waitForFirstLayout) { tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); } else { setRecyclerView(recyclerView); } } @Suppress @FlakyTest(bugId = 33949798) @Test @LargeTest public void hasPendingUpdatesBeforeFirstLayout() throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); TestLayoutManager layoutManager = new DumbLayoutManager(); TestAdapter testAdapter = new TestAdapter(10); setupBasic(recyclerView, layoutManager, testAdapter, false); assertTrue(mRecyclerView.hasPendingAdapterUpdates()); } @Test public void noPendingUpdatesAfterLayout() throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); TestLayoutManager layoutManager = new DumbLayoutManager(); TestAdapter testAdapter = new TestAdapter(10); setupBasic(recyclerView, layoutManager, testAdapter, true); assertFalse(mRecyclerView.hasPendingAdapterUpdates()); } @Test public void hasPendingUpdatesAfterItemIsRemoved() throws Throwable { assertPendingUpdatesAndLayoutTest(new AdapterRunnable() { @Override public void run(TestAdapter testAdapter) throws Throwable { testAdapter.deleteAndNotify(1, 1); } }); } @Test public void hasPendingUpdatesAfterItemIsInserted() throws Throwable { assertPendingUpdatesAndLayoutTest(new AdapterRunnable() { @Override public void run(TestAdapter testAdapter) throws Throwable { testAdapter.addAndNotify(2, 1); } }); } @Test public void hasPendingUpdatesAfterItemIsMoved() throws Throwable { assertPendingUpdatesAndLayoutTest(new AdapterRunnable() { @Override public void run(TestAdapter testAdapter) throws Throwable { testAdapter.moveItem(2, 3, true); } }); } @Test public void hasPendingUpdatesAfterItemIsChanged() throws Throwable { assertPendingUpdatesAndLayoutTest(new AdapterRunnable() { @Override public void run(TestAdapter testAdapter) throws Throwable { testAdapter.changeAndNotify(2, 1); } }); } @Test public void hasPendingUpdatesAfterDataSetIsChanged() throws Throwable { assertPendingUpdatesAndLayoutTest(new AdapterRunnable() { @Override public void run(TestAdapter testAdapter) { mRecyclerView.getAdapter().notifyDataSetChanged(); } }); } @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) @Test public void transientStateRecycleViaAdapter() throws Throwable { transientStateRecycleTest(true, false); } @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) @Test public void transientStateRecycleViaTransientStateCleanup() throws Throwable { transientStateRecycleTest(false, true); } @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) @Test public void transientStateDontRecycle() throws Throwable { transientStateRecycleTest(false, false); } public void transientStateRecycleTest(final boolean succeed, final boolean unsetTransientState) throws Throwable { final List<View> failedToRecycle = new ArrayList<>(); final List<View> recycled = new ArrayList<>(); TestAdapter testAdapter = new TestAdapter(10) { @Override public boolean onFailedToRecycleView(TestViewHolder holder) { failedToRecycle.add(holder.itemView); if (unsetTransientState) { setHasTransientState(holder.itemView, false); } return succeed; } @Override public void onViewRecycled(TestViewHolder holder) { recycled.add(holder.itemView); super.onViewRecycled(holder); } }; TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (getChildCount() == 0) { detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, 5); } else { removeAndRecycleAllViews(recycler); } if (layoutLatch != null) { layoutLatch.countDown(); } } }; RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(tlm); recyclerView.setItemAnimator(null); setRecyclerView(recyclerView); getInstrumentation().waitForIdleSync(); // make sure we have enough views after this position so that we'll receive the on recycled // callback View view = recyclerView.getChildAt(3);//this has to be greater than def cache size. setHasTransientState(view, true); tlm.expectLayouts(1); requestLayoutOnUIThread(recyclerView); tlm.waitForLayout(2); assertTrue(failedToRecycle.contains(view)); assertEquals(succeed || unsetTransientState, recycled.contains(view)); } @Test public void adapterPositionInvalidation() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final TestAdapter adapter = new TestAdapter(10); final TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutRange(recycler, 0, state.getItemCount()); layoutLatch.countDown(); } }; recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { for (int i = 0; i < tlm.getChildCount(); i++) { assertNotSame("adapter positions should not be undefined", recyclerView.getChildAdapterPosition(tlm.getChildAt(i)), RecyclerView.NO_POSITION); } adapter.notifyDataSetChanged(); for (int i = 0; i < tlm.getChildCount(); i++) { assertSame("adapter positions should be undefined", recyclerView.getChildAdapterPosition(tlm.getChildAt(i)), RecyclerView.NO_POSITION); } } }); } @Test public void adapterPositionsBasic() throws Throwable { adapterPositionsTest(null); } @Test public void adapterPositionsRemoveItems() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.deleteAndNotify(3, 4); } }); } @Test public void adapterPositionsRemoveItemsBefore() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.deleteAndNotify(0, 1); } }); } @Test public void adapterPositionsAddItemsBefore() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.addAndNotify(0, 5); } }); } @Test public void adapterPositionsAddItemsInside() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.addAndNotify(3, 2); } }); } @Test public void adapterPositionsMoveItems() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.moveAndNotify(3, 5); } }); } @Test public void adapterPositionsNotifyDataSetChanged() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.mItems.clear(); for (int i = 0; i < 20; i++) { adapter.mItems.add(new Item(i, "added item")); } adapter.notifyDataSetChanged(); } }); } @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) // transientState is API 16 @Test public void avoidLeakingRecyclerViewIfViewIsNotRecycled() throws Throwable { final AtomicBoolean failedToRecycle = new AtomicBoolean(false); final AtomicInteger recycledViewCount = new AtomicInteger(0); RecyclerView rv = new RecyclerView(getActivity()); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, state.getItemCount()); layoutLatch.countDown(); } }; TestAdapter adapter = new TestAdapter(10) { @Override public boolean onFailedToRecycleView( TestViewHolder holder) { failedToRecycle.set(true); return false; } @Override public void onViewRecycled(TestViewHolder holder) { recycledViewCount.incrementAndGet(); super.onViewRecycled(holder); } }; rv.setAdapter(adapter); rv.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(rv); tlm.waitForLayout(1); final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0)); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { ViewCompat.setHasTransientState(vh.itemView, true); } }); tlm.expectLayouts(1); adapter.deleteAndNotify(0, 10); tlm.waitForLayout(2); final CountDownLatch animationsLatch = new CountDownLatch(1); rv.getItemAnimator().isRunning( new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { @Override public void onAnimationsFinished() { animationsLatch.countDown(); } }); assertTrue(animationsLatch.await(2, TimeUnit.SECONDS)); assertThat(recycledViewCount.get(), is(9)); assertTrue(failedToRecycle.get()); assertNull(vh.mOwnerRecyclerView); checkForMainThreadException(); } @Test public void avoidLeakingRecyclerViewViaViewHolder() throws Throwable { RecyclerView rv = new RecyclerView(getActivity()); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, state.getItemCount()); layoutLatch.countDown(); } }; TestAdapter adapter = new TestAdapter(10); rv.setAdapter(adapter); rv.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(rv); tlm.waitForLayout(1); final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0)); tlm.expectLayouts(1); adapter.deleteAndNotify(0, 10); tlm.waitForLayout(2); final CountDownLatch animationsLatch = new CountDownLatch(1); rv.getItemAnimator().isRunning( new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { @Override public void onAnimationsFinished() { animationsLatch.countDown(); } }); assertTrue(animationsLatch.await(2, TimeUnit.SECONDS)); assertNull(vh.mOwnerRecyclerView); checkForMainThreadException(); } @Test public void duplicateAdapterPositionTest() throws Throwable { final TestAdapter testAdapter = new TestAdapter(10); final TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, state.getItemCount()); if (!state.isPreLayout()) { while (!recycler.getScrapList().isEmpty()) { RecyclerView.ViewHolder viewHolder = recycler.getScrapList().get(0); addDisappearingView(viewHolder.itemView, 0); } } layoutLatch.countDown(); } @Override public boolean supportsPredictiveItemAnimations() { return true; } }; final DefaultItemAnimator animator = new DefaultItemAnimator(); animator.setSupportsChangeAnimations(true); animator.setChangeDuration(10000); testAdapter.setHasStableIds(true); final TestRecyclerView recyclerView = new TestRecyclerView(getActivity()); recyclerView.setLayoutManager(tlm); recyclerView.setAdapter(testAdapter); recyclerView.setItemAnimator(animator); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(2); tlm.expectLayouts(2); testAdapter.mItems.get(2).mType += 2; final int itemId = testAdapter.mItems.get(2).mId; testAdapter.changeAndNotify(2, 1); tlm.waitForLayout(2); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { assertThat("test sanity", recyclerView.getChildCount(), CoreMatchers.is(11)); // now mangle the order and run the test RecyclerView.ViewHolder hidden = null; RecyclerView.ViewHolder updated = null; for (int i = 0; i < recyclerView.getChildCount(); i ++) { View view = recyclerView.getChildAt(i); RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); if (vh.getAdapterPosition() == 2) { if (mRecyclerView.mChildHelper.isHidden(view)) { assertThat(hidden, CoreMatchers.nullValue()); hidden = vh; } else { assertThat(updated, CoreMatchers.nullValue()); updated = vh; } } } assertThat(hidden, CoreMatchers.notNullValue()); assertThat(updated, CoreMatchers.notNullValue()); mRecyclerView.eatRequestLayout(); // first put the hidden child back int index1 = mRecyclerView.indexOfChild(hidden.itemView); int index2 = mRecyclerView.indexOfChild(updated.itemView); if (index1 < index2) { // swap views swapViewsAtIndices(recyclerView, index1, index2); } assertThat(tlm.findViewByPosition(2), CoreMatchers.sameInstance(updated.itemView)); assertThat(recyclerView.findViewHolderForAdapterPosition(2), CoreMatchers.sameInstance(updated)); assertThat(recyclerView.findViewHolderForLayoutPosition(2), CoreMatchers.sameInstance(updated)); assertThat(recyclerView.findViewHolderForItemId(itemId), CoreMatchers.sameInstance(updated)); // now swap back swapViewsAtIndices(recyclerView, index1, index2); assertThat(tlm.findViewByPosition(2), CoreMatchers.sameInstance(updated.itemView)); assertThat(recyclerView.findViewHolderForAdapterPosition(2), CoreMatchers.sameInstance(updated)); assertThat(recyclerView.findViewHolderForLayoutPosition(2), CoreMatchers.sameInstance(updated)); assertThat(recyclerView.findViewHolderForItemId(itemId), CoreMatchers.sameInstance(updated)); // now remove updated. re-assert fallback to the hidden one tlm.removeView(updated.itemView); assertThat(tlm.findViewByPosition(2), CoreMatchers.nullValue()); assertThat(recyclerView.findViewHolderForAdapterPosition(2), CoreMatchers.sameInstance(hidden)); assertThat(recyclerView.findViewHolderForLayoutPosition(2), CoreMatchers.sameInstance(hidden)); assertThat(recyclerView.findViewHolderForItemId(itemId), CoreMatchers.sameInstance(hidden)); } }); } private void swapViewsAtIndices(TestRecyclerView recyclerView, int index1, int index2) { if (index1 == index2) { return; } if (index2 < index1) { int tmp = index1; index1 = index2; index2 = tmp; } final View v1 = recyclerView.getChildAt(index1); final View v2 = recyclerView.getChildAt(index2); boolean v1Hidden = recyclerView.mChildHelper.isHidden(v1); boolean v2Hidden = recyclerView.mChildHelper.isHidden(v2); // must un-hide before swap otherwise bucket indices will become invalid. if (v1Hidden) { mRecyclerView.mChildHelper.unhide(v1); } if (v2Hidden) { mRecyclerView.mChildHelper.unhide(v2); } recyclerView.detachViewFromParent(index2); recyclerView.attachViewToParent(v2, index1, v2.getLayoutParams()); recyclerView.detachViewFromParent(index1 + 1); recyclerView.attachViewToParent(v1, index2, v1.getLayoutParams()); if (v1Hidden) { mRecyclerView.mChildHelper.hide(v1); } if (v2Hidden) { mRecyclerView.mChildHelper.hide(v2); } } public void adapterPositionsTest(final AdapterRunnable adapterChanges) throws Throwable { final TestAdapter testAdapter = new TestAdapter(10); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { layoutRange(recycler, Math.min(state.getItemCount(), 2) , Math.min(state.getItemCount(), 7)); layoutLatch.countDown(); } catch (Throwable t) { postExceptionToInstrumentation(t); } } }; final RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setLayoutManager(tlm); recyclerView.setAdapter(testAdapter); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { try { final int count = recyclerView.getChildCount(); Map<View, Integer> layoutPositions = new HashMap<>(); assertTrue("test sanity", count > 0); for (int i = 0; i < count; i++) { View view = recyclerView.getChildAt(i); TestViewHolder vh = (TestViewHolder) recyclerView.getChildViewHolder(view); int index = testAdapter.mItems.indexOf(vh.mBoundItem); assertEquals("should be able to find VH with adapter position " + index, vh, recyclerView.findViewHolderForAdapterPosition(index)); assertEquals("get adapter position should return correct index", index, vh.getAdapterPosition()); layoutPositions.put(view, vh.mPosition); } if (adapterChanges != null) { adapterChanges.run(testAdapter); for (int i = 0; i < count; i++) { View view = recyclerView.getChildAt(i); TestViewHolder vh = (TestViewHolder) recyclerView .getChildViewHolder(view); int index = testAdapter.mItems.indexOf(vh.mBoundItem); if (index >= 0) { assertEquals("should be able to find VH with adapter position " + index, vh, recyclerView.findViewHolderForAdapterPosition(index)); } assertSame("get adapter position should return correct index", index, vh.getAdapterPosition()); assertSame("should be able to find view with layout position", vh, mRecyclerView.findViewHolderForLayoutPosition( layoutPositions.get(view))); } } } catch (Throwable t) { postExceptionToInstrumentation(t); } } }); checkForMainThreadException(); } @Test public void scrollStateForSmoothScroll() throws Throwable { TestAdapter testAdapter = new TestAdapter(10); TestLayoutManager tlm = new TestLayoutManager(); RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(tlm); setRecyclerView(recyclerView); getInstrumentation().waitForIdleSync(); assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); final int[] stateCnts = new int[10]; final CountDownLatch latch = new CountDownLatch(2); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { stateCnts[newState] = stateCnts[newState] + 1; latch.countDown(); } }); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mRecyclerView.smoothScrollBy(0, 500); } }); latch.await(5, TimeUnit.SECONDS); assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); } @Test public void scrollStateForSmoothScrollWithStop() throws Throwable { TestAdapter testAdapter = new TestAdapter(10); TestLayoutManager tlm = new TestLayoutManager(); RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(tlm); setRecyclerView(recyclerView); getInstrumentation().waitForIdleSync(); assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); final int[] stateCnts = new int[10]; final CountDownLatch latch = new CountDownLatch(1); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { stateCnts[newState] = stateCnts[newState] + 1; latch.countDown(); } }); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mRecyclerView.smoothScrollBy(0, 500); } }); latch.await(5, TimeUnit.SECONDS); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mRecyclerView.stopScroll(); } }); assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); } @Test public void scrollStateForFling() throws Throwable { TestAdapter testAdapter = new TestAdapter(10); TestLayoutManager tlm = new TestLayoutManager(); RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(tlm); setRecyclerView(recyclerView); getInstrumentation().waitForIdleSync(); assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); final int[] stateCnts = new int[10]; final CountDownLatch latch = new CountDownLatch(2); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { stateCnts[newState] = stateCnts[newState] + 1; latch.countDown(); } }); final ViewConfiguration vc = ViewConfiguration.get(getActivity()); final float fling = vc.getScaledMinimumFlingVelocity() + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .1f; mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mRecyclerView.fling(0, Math.round(fling)); } }); latch.await(5, TimeUnit.SECONDS); assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); } @Test public void scrollStateForFlingWithStop() throws Throwable { TestAdapter testAdapter = new TestAdapter(10); TestLayoutManager tlm = new TestLayoutManager(); RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(tlm); setRecyclerView(recyclerView); getInstrumentation().waitForIdleSync(); assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); final int[] stateCnts = new int[10]; final CountDownLatch latch = new CountDownLatch(1); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { stateCnts[newState] = stateCnts[newState] + 1; latch.countDown(); } }); final ViewConfiguration vc = ViewConfiguration.get(getActivity()); final float fling = vc.getScaledMinimumFlingVelocity() + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .8f; mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mRecyclerView.fling(0, Math.round(fling)); } }); latch.await(5, TimeUnit.SECONDS); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mRecyclerView.stopScroll(); } }); assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); } @Test public void scrollStateDrag() throws Throwable { TestAdapter testAdapter = new TestAdapter(10); TestLayoutManager tlm = new TestLayoutManager(); RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(tlm); setRecyclerView(recyclerView); getInstrumentation().waitForIdleSync(); assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); final int[] stateCnts = new int[10]; recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { stateCnts[newState] = stateCnts[newState] + 1; } }); drag(mRecyclerView, 0, 0, 0, 500, 5); assertEquals(0, stateCnts[SCROLL_STATE_SETTLING]); assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); assertEquals(1, stateCnts[SCROLL_STATE_DRAGGING]); } public void drag(ViewGroup view, float fromX, float toX, float fromY, float toY, int stepCount) throws Throwable { long downTime = SystemClock.uptimeMillis(); long eventTime = SystemClock.uptimeMillis(); float y = fromY; float x = fromX; float yStep = (toY - fromY) / stepCount; float xStep = (toX - fromX) / stepCount; MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0); sendTouch(view, event); for (int i = 0; i < stepCount; ++i) { y += yStep; x += xStep; eventTime = SystemClock.uptimeMillis(); event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0); sendTouch(view, event); } eventTime = SystemClock.uptimeMillis(); event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); sendTouch(view, event); getInstrumentation().waitForIdleSync(); } private void sendTouch(final ViewGroup view, final MotionEvent event) throws Throwable { mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { if (view.onInterceptTouchEvent(event)) { view.onTouchEvent(event); } } }); } @Test public void recycleScrap() throws Throwable { recycleScrapTest(false); removeRecyclerView(); recycleScrapTest(true); } public void recycleScrapTest(final boolean useRecycler) throws Throwable { TestAdapter testAdapter = new TestAdapter(10); final AtomicBoolean test = new AtomicBoolean(false); TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ViewInfoStore infoStore = mRecyclerView.mViewInfoStore; if (test.get()) { try { detachAndScrapAttachedViews(recycler); for (int i = recycler.getScrapList().size() - 1; i >= 0; i--) { if (useRecycler) { recycler.recycleView(recycler.getScrapList().get(i).itemView); } else { removeAndRecycleView(recycler.getScrapList().get(i).itemView, recycler); } } if (infoStore.mOldChangedHolders != null) { for (int i = infoStore.mOldChangedHolders.size() - 1; i >= 0; i--) { if (useRecycler) { recycler.recycleView( infoStore.mOldChangedHolders.valueAt(i).itemView); } else { removeAndRecycleView( infoStore.mOldChangedHolders.valueAt(i).itemView, recycler); } } } assertEquals("no scrap should be left over", 0, recycler.getScrapCount()); assertEquals("pre layout map should be empty", 0, InfoStoreTrojan.sizeOfPreLayout(infoStore)); assertEquals("post layout map should be empty", 0, InfoStoreTrojan.sizeOfPostLayout(infoStore)); if (infoStore.mOldChangedHolders != null) { assertEquals("post old change map should be empty", 0, infoStore.mOldChangedHolders.size()); } } catch (Throwable t) { postExceptionToInstrumentation(t); } } layoutRange(recycler, 0, 5); layoutLatch.countDown(); super.onLayoutChildren(recycler, state); } }; RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(lm); ((SimpleItemAnimator)recyclerView.getItemAnimator()).setSupportsChangeAnimations(true); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); test.set(true); lm.expectLayouts(1); testAdapter.changeAndNotify(3, 1); lm.waitForLayout(2); checkForMainThreadException(); } @Test public void aAccessRecyclerOnOnMeasureWithPredictive() throws Throwable { accessRecyclerOnOnMeasureTest(true); } @Test public void accessRecyclerOnOnMeasureWithoutPredictive() throws Throwable { accessRecyclerOnOnMeasureTest(false); } @Test public void smoothScrollWithRemovedItemsAndRemoveItem() throws Throwable { smoothScrollTest(true); } @Test public void smoothScrollWithRemovedItems() throws Throwable { smoothScrollTest(false); } public void smoothScrollTest(final boolean removeItem) throws Throwable { final LinearSmoothScroller[] lss = new LinearSmoothScroller[1]; final CountDownLatch calledOnStart = new CountDownLatch(1); final CountDownLatch calledOnStop = new CountDownLatch(1); final int visibleChildCount = 10; TestLayoutManager lm = new TestLayoutManager() { int start = 0; @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); layoutRange(recycler, start, visibleChildCount); layoutLatch.countDown(); } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { start++; if (DEBUG) { Log.d(TAG, "on scroll, remove and recycling. start:" + start + ", cnt:" + visibleChildCount); } removeAndRecycleAllViews(recycler); layoutRange(recycler, start, Math.max(state.getItemCount(), start + visibleChildCount)); return dy; } @Override public boolean canScrollVertically() { return true; } @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { @Override public PointF computeScrollVectorForPosition(int targetPosition) { return new PointF(0, 1); } @Override protected void onStart() { super.onStart(); calledOnStart.countDown(); } @Override protected void onStop() { super.onStop(); calledOnStop.countDown(); } }; linearSmoothScroller.setTargetPosition(position); lss[0] = linearSmoothScroller; startSmoothScroll(linearSmoothScroller); } }; final RecyclerView rv = new RecyclerView(getActivity()); TestAdapter testAdapter = new TestAdapter(500); rv.setLayoutManager(lm); rv.setAdapter(testAdapter); lm.expectLayouts(1); setRecyclerView(rv); lm.waitForLayout(1); // regular scroll final int targetPosition = visibleChildCount * (removeItem ? 30 : 4); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { rv.smoothScrollToPosition(targetPosition); } }); if (DEBUG) { Log.d(TAG, "scrolling to target position " + targetPosition); } assertTrue("on start should be called very soon", calledOnStart.await(2, TimeUnit.SECONDS)); if (removeItem) { final int newTarget = targetPosition - 10; testAdapter.deleteAndNotify(newTarget + 1, testAdapter.getItemCount() - newTarget - 1); final CountDownLatch targetCheck = new CountDownLatch(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { ViewCompat.postOnAnimationDelayed(rv, new Runnable() { @Override public void run() { try { assertEquals("scroll position should be updated to next available", newTarget, lss[0].getTargetPosition()); } catch (Throwable t) { postExceptionToInstrumentation(t); } targetCheck.countDown(); } }, 50); } }); assertTrue("target position should be checked on time ", targetCheck.await(10, TimeUnit.SECONDS)); checkForMainThreadException(); assertTrue("on stop should be called", calledOnStop.await(30, TimeUnit.SECONDS)); checkForMainThreadException(); assertNotNull("should scroll to new target " + newTarget , rv.findViewHolderForLayoutPosition(newTarget)); if (DEBUG) { Log.d(TAG, "on stop has been called on time"); } } else { assertTrue("on stop should be called eventually", calledOnStop.await(30, TimeUnit.SECONDS)); assertNotNull("scroll to position should succeed", rv.findViewHolderForLayoutPosition(targetPosition)); } checkForMainThreadException(); } @Test public void consecutiveSmoothScroll() throws Throwable { final AtomicInteger visibleChildCount = new AtomicInteger(10); final AtomicInteger totalScrolled = new AtomicInteger(0); final TestLayoutManager lm = new TestLayoutManager() { int start = 0; @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); layoutRange(recycler, start, visibleChildCount.get()); layoutLatch.countDown(); } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { totalScrolled.set(totalScrolled.get() + dy); return dy; } @Override public boolean canScrollVertically() { return true; } }; final RecyclerView rv = new RecyclerView(getActivity()); TestAdapter testAdapter = new TestAdapter(500); rv.setLayoutManager(lm); rv.setAdapter(testAdapter); lm.expectLayouts(1); setRecyclerView(rv); lm.waitForLayout(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { rv.smoothScrollBy(0, 2000); } }); Thread.sleep(250); final AtomicInteger scrollAmt = new AtomicInteger(); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { final int soFar = totalScrolled.get(); scrollAmt.set(soFar); rv.smoothScrollBy(0, 5000 - soFar); } }); while (rv.getScrollState() != SCROLL_STATE_IDLE) { Thread.sleep(100); } final int soFar = totalScrolled.get(); assertEquals("second scroll should be competed properly", 5000, soFar); } public void accessRecyclerOnOnMeasureTest(final boolean enablePredictiveAnimations) throws Throwable { TestAdapter testAdapter = new TestAdapter(10); final AtomicInteger expectedOnMeasureStateCount = new AtomicInteger(10); TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); try { layoutRange(recycler, 0, state.getItemCount()); layoutLatch.countDown(); } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } } @Override public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) { try { // make sure we access all views for (int i = 0; i < state.getItemCount(); i++) { View view = recycler.getViewForPosition(i); assertNotNull(view); assertEquals(i, getPosition(view)); } if (!state.isPreLayout()) { assertEquals(state.toString(), expectedOnMeasureStateCount.get(), state.getItemCount()); } } catch (Throwable t) { postExceptionToInstrumentation(t); } super.onMeasure(recycler, state, widthSpec, heightSpec); } @Override public boolean supportsPredictiveItemAnimations() { return enablePredictiveAnimations; } }; RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setLayoutManager(lm); recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(lm); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); checkForMainThreadException(); lm.expectLayouts(1); if (!enablePredictiveAnimations) { expectedOnMeasureStateCount.set(15); } testAdapter.addAndNotify(4, 5); lm.waitForLayout(2); checkForMainThreadException(); } @Test public void setCompatibleAdapter() throws Throwable { compatibleAdapterTest(true, true); removeRecyclerView(); compatibleAdapterTest(false, true); removeRecyclerView(); compatibleAdapterTest(true, false); removeRecyclerView(); compatibleAdapterTest(false, false); removeRecyclerView(); } private void compatibleAdapterTest(boolean useCustomPool, boolean removeAndRecycleExistingViews) throws Throwable { TestAdapter testAdapter = new TestAdapter(10); final AtomicInteger recycledViewCount = new AtomicInteger(); TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { layoutRange(recycler, 0, state.getItemCount()); layoutLatch.countDown(); } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } } }; RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setLayoutManager(lm); recyclerView.setAdapter(testAdapter); recyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() { @Override public void onViewRecycled(RecyclerView.ViewHolder holder) { recycledViewCount.incrementAndGet(); } }); lm.expectLayouts(1); setRecyclerView(recyclerView, !useCustomPool); lm.waitForLayout(2); checkForMainThreadException(); lm.expectLayouts(1); swapAdapter(new TestAdapter(10), removeAndRecycleExistingViews); lm.waitForLayout(2); checkForMainThreadException(); if (removeAndRecycleExistingViews) { assertTrue("Previous views should be recycled", recycledViewCount.get() > 0); } else { assertEquals("No views should be recycled if adapters are compatible and developer " + "did not request a recycle", 0, recycledViewCount.get()); } } @Test public void setIncompatibleAdapter() throws Throwable { incompatibleAdapterTest(true); incompatibleAdapterTest(false); } public void incompatibleAdapterTest(boolean useCustomPool) throws Throwable { TestAdapter testAdapter = new TestAdapter(10); TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); try { layoutRange(recycler, 0, state.getItemCount()); layoutLatch.countDown(); } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } } }; RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setLayoutManager(lm); recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(lm); lm.expectLayouts(1); setRecyclerView(recyclerView, !useCustomPool); lm.waitForLayout(2); checkForMainThreadException(); lm.expectLayouts(1); setAdapter(new TestAdapter2(10)); lm.waitForLayout(2); checkForMainThreadException(); } @Test public void recycleIgnored() throws Throwable { final TestAdapter adapter = new TestAdapter(10); final TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { layoutRange(recycler, 0, 5); layoutLatch.countDown(); } }; final RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(lm); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { View child1 = lm.findViewByPosition(0); View child2 = lm.findViewByPosition(1); lm.ignoreView(child1); lm.ignoreView(child2); lm.removeAndRecycleAllViews(recyclerView.mRecycler); assertEquals("ignored child should not be recycled or removed", 2, lm.getChildCount()); Throwable[] throwables = new Throwable[1]; try { lm.removeAndRecycleView(child1, mRecyclerView.mRecycler); } catch (Throwable t) { throwables[0] = t; } assertTrue("Trying to recycle an ignored view should throw IllegalArgException " , throwables[0] instanceof IllegalArgumentException); lm.removeAllViews(); assertEquals("ignored child should be removed as well ", 0, lm.getChildCount()); } }); } @Test public void findIgnoredByPosition() throws Throwable { final TestAdapter adapter = new TestAdapter(10); final TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, 5); layoutLatch.countDown(); } }; final RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(lm); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); Thread.sleep(5000); final int pos = 1; final View[] ignored = new View[1]; mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { View child = lm.findViewByPosition(pos); lm.ignoreView(child); ignored[0] = child; } }); assertNotNull("ignored child should not be null", ignored[0]); assertNull("find view by position should not return ignored child", lm.findViewByPosition(pos)); lm.expectLayouts(1); requestLayoutOnUIThread(mRecyclerView); lm.waitForLayout(1); assertEquals("child count should be ", 6, lm.getChildCount()); View replacement = lm.findViewByPosition(pos); assertNotNull("re-layout should replace ignored child w/ another one", replacement); assertNotSame("replacement should be a different view", replacement, ignored[0]); } @Test public void itemDecorsWithPredictive() throws Throwable { LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true); lm.setSupportsPredictive(true); final Object changePayload = new Object(); final TestAdapter adapter = new TestAdapter(10) { @Override public void onBindViewHolder(TestViewHolder holder, int position, List<Object> payloads) { super.onBindViewHolder(holder, position); holder.setData(payloads.isEmpty() ? null : payloads.get(0)); } }; final Map<Integer, Object> preLayoutData = new HashMap<>(); final Map<Integer, Object> postLayoutData = new HashMap<>(); final RecyclerView.ItemDecoration decoration = new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { try { TestViewHolder tvh = (TestViewHolder) parent.getChildViewHolder(view); Object data = tvh.getData(); int adapterPos = tvh.getAdapterPosition(); assertThat(adapterPos, is(not(NO_POSITION))); if (state.isPreLayout()) { preLayoutData.put(adapterPos, data); } else { postLayoutData.put(adapterPos, data); } } catch (Throwable t) { postExceptionToInstrumentation(t); } } }; RecyclerView rv = new RecyclerView(getActivity()); rv.addItemDecoration(decoration); rv.setAdapter(adapter); rv.setLayoutManager(lm); lm.expectLayouts(1); setRecyclerView(rv); lm.waitForLayout(2); preLayoutData.clear(); postLayoutData.clear(); lm.expectLayouts(2); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { adapter.notifyItemChanged(3, changePayload); } }); lm.waitForLayout(2); assertThat(preLayoutData.containsKey(3), is(false)); assertThat(postLayoutData.get(3), is(changePayload)); assertThat(preLayoutData.size(), is(0)); assertThat(postLayoutData.size(), is(1)); checkForMainThreadException(); } @Test public void invalidateAllDecorOffsets() throws Throwable { final TestAdapter adapter = new TestAdapter(10); final RecyclerView recyclerView = new RecyclerView(getActivity()); final AtomicBoolean invalidatedOffsets = new AtomicBoolean(true); recyclerView.setAdapter(adapter); final AtomicInteger layoutCount = new AtomicInteger(4); final RecyclerView.ItemDecoration dummyItemDecoration = new RecyclerView.ItemDecoration() { }; TestLayoutManager testLayoutManager = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { // test for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); assertEquals( "Decor insets validation for VH should have expected value.", invalidatedOffsets.get(), lp.mInsetsDirty); } for (RecyclerView.ViewHolder vh : mRecyclerView.mRecycler.mCachedViews) { RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) vh.itemView.getLayoutParams(); assertEquals( "Decor insets invalidation in cache for VH should have expected " + "value.", invalidatedOffsets.get(), lp.mInsetsDirty); } detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, layoutCount.get()); } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } } @Override public boolean supportsPredictiveItemAnimations() { return false; } }; // first layout recyclerView.setItemViewCacheSize(5); recyclerView.setLayoutManager(testLayoutManager); testLayoutManager.expectLayouts(1); setRecyclerView(recyclerView, true, false); testLayoutManager.waitForLayout(2); checkForMainThreadException(); // re-layout w/o any change invalidatedOffsets.set(false); testLayoutManager.expectLayouts(1); requestLayoutOnUIThread(recyclerView); testLayoutManager.waitForLayout(1); checkForMainThreadException(); // invalidate w/o an item decorator invalidateDecorOffsets(recyclerView); testLayoutManager.expectLayouts(1); invalidateDecorOffsets(recyclerView); testLayoutManager.assertNoLayout("layout should not happen", 2); checkForMainThreadException(); // set item decorator, should invalidate invalidatedOffsets.set(true); testLayoutManager.expectLayouts(1); addItemDecoration(mRecyclerView, dummyItemDecoration); testLayoutManager.waitForLayout(1); checkForMainThreadException(); // re-layout w/o any change invalidatedOffsets.set(false); testLayoutManager.expectLayouts(1); requestLayoutOnUIThread(recyclerView); testLayoutManager.waitForLayout(1); checkForMainThreadException(); // invalidate w/ item decorator invalidatedOffsets.set(true); invalidateDecorOffsets(recyclerView); testLayoutManager.expectLayouts(1); invalidateDecorOffsets(recyclerView); testLayoutManager.waitForLayout(2); checkForMainThreadException(); // trigger cache. layoutCount.set(3); invalidatedOffsets.set(false); testLayoutManager.expectLayouts(1); requestLayoutOnUIThread(mRecyclerView); testLayoutManager.waitForLayout(1); checkForMainThreadException(); assertEquals("a view should be cached", 1, mRecyclerView.mRecycler.mCachedViews.size()); layoutCount.set(5); invalidatedOffsets.set(true); testLayoutManager.expectLayouts(1); invalidateDecorOffsets(recyclerView); testLayoutManager.waitForLayout(1); checkForMainThreadException(); // remove item decorator invalidatedOffsets.set(true); testLayoutManager.expectLayouts(1); removeItemDecoration(mRecyclerView, dummyItemDecoration); testLayoutManager.waitForLayout(1); checkForMainThreadException(); } public void addItemDecoration(final RecyclerView recyclerView, final RecyclerView.ItemDecoration itemDecoration) throws Throwable { mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { recyclerView.addItemDecoration(itemDecoration); } }); } public void removeItemDecoration(final RecyclerView recyclerView, final RecyclerView.ItemDecoration itemDecoration) throws Throwable { mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { recyclerView.removeItemDecoration(itemDecoration); } }); } public void invalidateDecorOffsets(final RecyclerView recyclerView) throws Throwable { mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { recyclerView.invalidateItemDecorations(); } }); } @Test public void invalidateDecorOffsets() throws Throwable { final TestAdapter adapter = new TestAdapter(10); adapter.setHasStableIds(true); final RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(adapter); final Map<Long, Boolean> changes = new HashMap<>(); TestLayoutManager testLayoutManager = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { if (changes.size() > 0) { // test for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); RecyclerView.ViewHolder vh = lp.mViewHolder; if (!changes.containsKey(vh.getItemId())) { continue; //nothing to test } assertEquals( "Decor insets validation for VH should have expected value.", changes.get(vh.getItemId()), lp.mInsetsDirty); } } detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, state.getItemCount()); } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } } @Override public boolean supportsPredictiveItemAnimations() { return false; } }; recyclerView.setLayoutManager(testLayoutManager); testLayoutManager.expectLayouts(1); setRecyclerView(recyclerView); testLayoutManager.waitForLayout(2); int itemAddedTo = 5; for (int i = 0; i < itemAddedTo; i++) { changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false); } for (int i = itemAddedTo; i < mRecyclerView.getChildCount(); i++) { changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true); } testLayoutManager.expectLayouts(1); adapter.addAndNotify(5, 1); testLayoutManager.waitForLayout(2); checkForMainThreadException(); changes.clear(); int[] changedItems = new int[]{3, 5, 6}; for (int i = 0; i < adapter.getItemCount(); i++) { changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false); } for (int changedItem : changedItems) { changes.put(mRecyclerView.findViewHolderForLayoutPosition(changedItem).getItemId(), true); } testLayoutManager.expectLayouts(1); adapter.changePositionsAndNotify(changedItems); testLayoutManager.waitForLayout(2); checkForMainThreadException(); for (int i = 0; i < adapter.getItemCount(); i++) { changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true); } testLayoutManager.expectLayouts(1); adapter.dispatchDataSetChanged(); testLayoutManager.waitForLayout(2); checkForMainThreadException(); } @Test public void movingViaStableIds() throws Throwable { stableIdsMoveTest(true); removeRecyclerView(); stableIdsMoveTest(false); removeRecyclerView(); } public void stableIdsMoveTest(final boolean supportsPredictive) throws Throwable { final TestAdapter testAdapter = new TestAdapter(10); testAdapter.setHasStableIds(true); final AtomicBoolean test = new AtomicBoolean(false); final int movedViewFromIndex = 3; final int movedViewToIndex = 6; final View[] movedView = new View[1]; TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); try { if (test.get()) { if (state.isPreLayout()) { View view = recycler.getViewForPosition(movedViewFromIndex, true); assertSame("In pre layout, should be able to get moved view w/ old " + "position", movedView[0], view); RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); assertTrue("it should come from scrap", holder.wasReturnedFromScrap()); // clear scrap flag holder.clearReturnedFromScrapFlag(); } else { View view = recycler.getViewForPosition(movedViewToIndex, true); assertSame("In post layout, should be able to get moved view w/ new " + "position", movedView[0], view); RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); assertTrue("it should come from scrap", holder.wasReturnedFromScrap()); // clear scrap flag holder.clearReturnedFromScrapFlag(); } } layoutRange(recycler, 0, state.getItemCount()); } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } } @Override public boolean supportsPredictiveItemAnimations() { return supportsPredictive; } }; RecyclerView recyclerView = new RecyclerView(this.getActivity()); recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(lm); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(1); movedView[0] = recyclerView.getChildAt(movedViewFromIndex); test.set(true); lm.expectLayouts(supportsPredictive ? 2 : 1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { Item item = testAdapter.mItems.remove(movedViewFromIndex); testAdapter.mItems.add(movedViewToIndex, item); testAdapter.notifyItemRemoved(movedViewFromIndex); testAdapter.notifyItemInserted(movedViewToIndex); } }); lm.waitForLayout(2); checkForMainThreadException(); } @Test public void adapterChangeDuringLayout() throws Throwable { adapterChangeInMainThreadTest("notifyDataSetChanged", new Runnable() { @Override public void run() { mRecyclerView.getAdapter().notifyDataSetChanged(); } }); adapterChangeInMainThreadTest("notifyItemChanged", new Runnable() { @Override public void run() { mRecyclerView.getAdapter().notifyItemChanged(2); } }); adapterChangeInMainThreadTest("notifyItemInserted", new Runnable() { @Override public void run() { mRecyclerView.getAdapter().notifyItemInserted(2); } }); adapterChangeInMainThreadTest("notifyItemRemoved", new Runnable() { @Override public void run() { mRecyclerView.getAdapter().notifyItemRemoved(2); } }); } public void adapterChangeInMainThreadTest(String msg, final Runnable onLayoutRunnable) throws Throwable { setIgnoreMainThreadException(true); final AtomicBoolean doneFirstLayout = new AtomicBoolean(false); TestAdapter testAdapter = new TestAdapter(10); TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); try { layoutRange(recycler, 0, state.getItemCount()); if (doneFirstLayout.get()) { onLayoutRunnable.run(); } } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } } }; RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setLayoutManager(lm); recyclerView.setAdapter(testAdapter); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); doneFirstLayout.set(true); lm.expectLayouts(1); requestLayoutOnUIThread(recyclerView); lm.waitForLayout(2); removeRecyclerView(); assertTrue("Invalid data updates should be caught:" + msg, getMainThreadException() instanceof IllegalStateException); } @Test public void adapterChangeDuringScroll() throws Throwable { for (int orientation : new int[]{OrientationHelper.HORIZONTAL, OrientationHelper.VERTICAL}) { adapterChangeDuringScrollTest("notifyDataSetChanged", orientation, new Runnable() { @Override public void run() { mRecyclerView.getAdapter().notifyDataSetChanged(); } }); adapterChangeDuringScrollTest("notifyItemChanged", orientation, new Runnable() { @Override public void run() { mRecyclerView.getAdapter().notifyItemChanged(2); } }); adapterChangeDuringScrollTest("notifyItemInserted", orientation, new Runnable() { @Override public void run() { mRecyclerView.getAdapter().notifyItemInserted(2); } }); adapterChangeDuringScrollTest("notifyItemRemoved", orientation, new Runnable() { @Override public void run() { mRecyclerView.getAdapter().notifyItemRemoved(2); } }); } } public void adapterChangeDuringScrollTest(String msg, final int orientation, final Runnable onScrollRunnable) throws Throwable { setIgnoreMainThreadException(true); TestAdapter testAdapter = new TestAdapter(100); TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); try { layoutRange(recycler, 0, 10); } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } } @Override public boolean canScrollVertically() { return orientation == OrientationHelper.VERTICAL; } @Override public boolean canScrollHorizontally() { return orientation == OrientationHelper.HORIZONTAL; } public int mockScroll() { try { onScrollRunnable.run(); } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } return 0; } @Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { return mockScroll(); } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { return mockScroll(); } }; RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setLayoutManager(lm); recyclerView.setAdapter(testAdapter); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); lm.expectLayouts(1); scrollBy(200); lm.waitForLayout(2); removeRecyclerView(); assertTrue("Invalid data updates should be caught:" + msg, getMainThreadException() instanceof IllegalStateException); } @Test public void recycleOnDetach() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final TestAdapter testAdapter = new TestAdapter(10); final AtomicBoolean didRunOnDetach = new AtomicBoolean(false); final TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); layoutRange(recycler, 0, state.getItemCount() - 1); layoutLatch.countDown(); } @Override public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { super.onDetachedFromWindow(view, recycler); didRunOnDetach.set(true); removeAndRecycleAllViews(recycler); } }; recyclerView.setAdapter(testAdapter); recyclerView.setLayoutManager(lm); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); removeRecyclerView(); assertTrue("When recycler view is removed, detach should run", didRunOnDetach.get()); assertEquals("All children should be recycled", recyclerView.getChildCount(), 0); } @Test public void updatesWhileDetached() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final int initialAdapterSize = 20; final TestAdapter adapter = new TestAdapter(initialAdapterSize); final AtomicInteger layoutCount = new AtomicInteger(0); TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); layoutRange(recycler, 0, 5); layoutCount.incrementAndGet(); layoutLatch.countDown(); } }; recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(lm); recyclerView.setHasFixedSize(true); lm.expectLayouts(1); adapter.addAndNotify(4, 5); lm.assertNoLayout("When RV is not attached, layout should not happen", 1); } @Test public void updatesAfterDetach() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final int initialAdapterSize = 20; final TestAdapter adapter = new TestAdapter(initialAdapterSize); final AtomicInteger layoutCount = new AtomicInteger(0); TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); layoutRange(recycler, 0, 5); layoutCount.incrementAndGet(); layoutLatch.countDown(); } }; recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(lm); lm.expectLayouts(1); recyclerView.setHasFixedSize(true); setRecyclerView(recyclerView); lm.waitForLayout(2); lm.expectLayouts(1); final int prevLayoutCount = layoutCount.get(); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { try { adapter.addAndNotify(4, 5); removeRecyclerView(); } catch (Throwable throwable) { postExceptionToInstrumentation(throwable); } } }); checkForMainThreadException(); lm.assertNoLayout("When RV is not attached, layout should not happen", 1); assertEquals("No extra layout should happen when detached", prevLayoutCount, layoutCount.get()); } @Test public void notifyDataSetChangedWithStableIds() throws Throwable { final Map<Integer, Integer> oldPositionToNewPositionMapping = new HashMap<>(); final TestAdapter adapter = new TestAdapter(100) { @Override public long getItemId(int position) { return mItems.get(position).mId; } }; adapter.setHasStableIds(true); final ArrayList<Item> previousItems = new ArrayList<>(); previousItems.addAll(adapter.mItems); final AtomicInteger layoutStart = new AtomicInteger(50); final AtomicBoolean validate = new AtomicBoolean(false); final int childCount = 10; final TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { super.onLayoutChildren(recycler, state); if (validate.get()) { assertEquals("Cached views should be kept", 5, recycler .mCachedViews.size()); for (RecyclerView.ViewHolder vh : recycler.mCachedViews) { TestViewHolder tvh = (TestViewHolder) vh; assertTrue("view holder should be marked for update", tvh.needsUpdate()); assertTrue("view holder should be marked as invalid", tvh.isInvalid()); } } detachAndScrapAttachedViews(recycler); if (validate.get()) { assertEquals("cache size should stay the same", 5, recycler.mCachedViews.size()); assertEquals("all views should be scrapped", childCount, recycler.getScrapList().size()); for (RecyclerView.ViewHolder vh : recycler.getScrapList()) { // TODO create test case for type change TestViewHolder tvh = (TestViewHolder) vh; assertTrue("view holder should be marked for update", tvh.needsUpdate()); assertTrue("view holder should be marked as invalid", tvh.isInvalid()); } } layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount); if (validate.get()) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); TestViewHolder tvh = (TestViewHolder) mRecyclerView .getChildViewHolder(view); final int oldPos = previousItems.indexOf(tvh.mBoundItem); assertEquals("view holder's position should be correct", oldPositionToNewPositionMapping.get(oldPos).intValue(), tvh.getLayoutPosition()); } } } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } } }; final RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setItemAnimator(null); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(lm); recyclerView.setItemViewCacheSize(10); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); checkForMainThreadException(); getInstrumentation().waitForIdleSync(); layoutStart.set(layoutStart.get() + 5);//55 lm.expectLayouts(1); requestLayoutOnUIThread(recyclerView); lm.waitForLayout(2); validate.set(true); lm.expectLayouts(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { try { adapter.moveItems(false, new int[]{50, 56}, new int[]{51, 1}, new int[]{52, 2}, new int[]{53, 54}, new int[]{60, 61}, new int[]{62, 64}, new int[]{75, 58}); for (int i = 0; i < previousItems.size(); i++) { Item item = previousItems.get(i); oldPositionToNewPositionMapping.put(i, adapter.mItems.indexOf(item)); } adapter.dispatchDataSetChanged(); } catch (Throwable throwable) { postExceptionToInstrumentation(throwable); } } }); lm.waitForLayout(2); checkForMainThreadException(); } @Test public void callbacksDuringAdapterSwap() throws Throwable { callbacksDuringAdapterChange(true); } @Test public void callbacksDuringAdapterSet() throws Throwable { callbacksDuringAdapterChange(false); } public void callbacksDuringAdapterChange(boolean swap) throws Throwable { final TestAdapter2 adapter1 = swap ? createBinderCheckingAdapter() : createOwnerCheckingAdapter(); final TestAdapter2 adapter2 = swap ? createBinderCheckingAdapter() : createOwnerCheckingAdapter(); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { layoutRange(recycler, 0, state.getItemCount()); } catch (Throwable t) { postExceptionToInstrumentation(t); } layoutLatch.countDown(); } }; RecyclerView rv = new RecyclerView(getActivity()); rv.setAdapter(adapter1); rv.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(rv); tlm.waitForLayout(1); checkForMainThreadException(); tlm.expectLayouts(1); if (swap) { swapAdapter(adapter2, true); } else { setAdapter(adapter2); } checkForMainThreadException(); tlm.waitForLayout(1); checkForMainThreadException(); } private TestAdapter2 createOwnerCheckingAdapter() { return new TestAdapter2(10) { @Override public void onViewRecycled(TestViewHolder2 holder) { assertSame("on recycled should be called w/ the creator adapter", this, holder.mData); super.onViewRecycled(holder); } @Override public void onBindViewHolder(TestViewHolder2 holder, int position) { super.onBindViewHolder(holder, position); assertSame("on bind should be called w/ the creator adapter", this, holder.mData); } @Override public TestViewHolder2 onCreateViewHolder(ViewGroup parent, int viewType) { final TestViewHolder2 vh = super.onCreateViewHolder(parent, viewType); vh.mData = this; return vh; } }; } private TestAdapter2 createBinderCheckingAdapter() { return new TestAdapter2(10) { @Override public void onViewRecycled(TestViewHolder2 holder) { assertSame("on recycled should be called w/ the creator adapter", this, holder.mData); holder.mData = null; super.onViewRecycled(holder); } @Override public void onBindViewHolder(TestViewHolder2 holder, int position) { super.onBindViewHolder(holder, position); holder.mData = this; } }; } @Test public void findViewById() throws Throwable { findViewByIdTest(false); removeRecyclerView(); findViewByIdTest(true); } public void findViewByIdTest(final boolean supportPredictive) throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final int initialAdapterSize = 20; final TestAdapter adapter = new TestAdapter(initialAdapterSize); final int deleteStart = 6; final int deleteCount = 5; recyclerView.setAdapter(adapter); final AtomicBoolean assertPositions = new AtomicBoolean(false); TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); if (assertPositions.get()) { if (state.isPreLayout()) { for (int i = 0; i < deleteStart; i++) { View view = findViewByPosition(i); assertNotNull("find view by position for existing items should work " + "fine", view); assertFalse("view should not be marked as removed", ((RecyclerView.LayoutParams) view.getLayoutParams()) .isItemRemoved()); } for (int i = 0; i < deleteCount; i++) { View view = findViewByPosition(i + deleteStart); assertNotNull("find view by position should work fine for removed " + "views in pre-layout", view); assertTrue("view should be marked as removed", ((RecyclerView.LayoutParams) view.getLayoutParams()) .isItemRemoved()); } for (int i = deleteStart + deleteCount; i < 20; i++) { View view = findViewByPosition(i); assertNotNull(view); assertFalse("view should not be marked as removed", ((RecyclerView.LayoutParams) view.getLayoutParams()) .isItemRemoved()); } } else { for (int i = 0; i < initialAdapterSize - deleteCount; i++) { View view = findViewByPosition(i); assertNotNull("find view by position for existing item " + i + " should work fine. child count:" + getChildCount(), view); TestViewHolder viewHolder = (TestViewHolder) mRecyclerView.getChildViewHolder(view); assertSame("should be the correct item " + viewHolder , viewHolder.mBoundItem, adapter.mItems.get(viewHolder.mPosition)); assertFalse("view should not be marked as removed", ((RecyclerView.LayoutParams) view.getLayoutParams()) .isItemRemoved()); } } } detachAndScrapAttachedViews(recycler); layoutRange(recycler, state.getItemCount() - 1, -1); layoutLatch.countDown(); } @Override public boolean supportsPredictiveItemAnimations() { return supportPredictive; } }; recyclerView.setLayoutManager(lm); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); getInstrumentation().waitForIdleSync(); assertPositions.set(true); lm.expectLayouts(supportPredictive ? 2 : 1); adapter.deleteAndNotify(new int[]{deleteStart, deleteCount - 1}, new int[]{deleteStart, 1}); lm.waitForLayout(2); } @Test public void typeForCache() throws Throwable { final AtomicInteger viewType = new AtomicInteger(1); final TestAdapter adapter = new TestAdapter(100) { @Override public int getItemViewType(int position) { return viewType.get(); } @Override public long getItemId(int position) { return mItems.get(position).mId; } }; adapter.setHasStableIds(true); final AtomicInteger layoutStart = new AtomicInteger(2); final int childCount = 10; final TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); detachAndScrapAttachedViews(recycler); layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount); layoutLatch.countDown(); } }; final RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setItemAnimator(null); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(lm); recyclerView.setItemViewCacheSize(10); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); getInstrumentation().waitForIdleSync(); layoutStart.set(4); // trigger a cache for 3,4 lm.expectLayouts(1); requestLayoutOnUIThread(recyclerView); lm.waitForLayout(2); // viewType.incrementAndGet(); layoutStart.set(2); // go back to bring views from cache lm.expectLayouts(1); adapter.mItems.remove(1); adapter.dispatchDataSetChanged(); lm.waitForLayout(2); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { for (int i = 2; i < 4; i++) { RecyclerView.ViewHolder vh = recyclerView.findViewHolderForLayoutPosition(i); assertEquals("View holder's type should match latest type", viewType.get(), vh.getItemViewType()); } } }); } @Test public void typeForExistingViews() throws Throwable { final AtomicInteger viewType = new AtomicInteger(1); final int invalidatedCount = 2; final int layoutStart = 2; final TestAdapter adapter = new TestAdapter(100) { @Override public int getItemViewType(int position) { return viewType.get(); } @Override public void onBindViewHolder(TestViewHolder holder, int position) { super.onBindViewHolder(holder, position); if (position >= layoutStart && position < invalidatedCount + layoutStart) { try { assertEquals("holder type should match current view type at position " + position, viewType.get(), holder.getItemViewType()); } catch (Throwable t) { postExceptionToInstrumentation(t); } } } @Override public long getItemId(int position) { return mItems.get(position).mId; } }; adapter.setHasStableIds(true); final int childCount = 10; final TestLayoutManager lm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); detachAndScrapAttachedViews(recycler); layoutRange(recycler, layoutStart, layoutStart + childCount); layoutLatch.countDown(); } }; final RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(lm); lm.expectLayouts(1); setRecyclerView(recyclerView); lm.waitForLayout(2); getInstrumentation().waitForIdleSync(); viewType.incrementAndGet(); lm.expectLayouts(1); adapter.changeAndNotify(layoutStart, invalidatedCount); lm.waitForLayout(2); checkForMainThreadException(); } @Test public void state() throws Throwable { final TestAdapter adapter = new TestAdapter(10); final RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(adapter); recyclerView.setItemAnimator(null); final AtomicInteger itemCount = new AtomicInteger(); final AtomicBoolean structureChanged = new AtomicBoolean(); TestLayoutManager testLayoutManager = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, state.getItemCount()); itemCount.set(state.getItemCount()); structureChanged.set(state.didStructureChange()); layoutLatch.countDown(); } }; recyclerView.setLayoutManager(testLayoutManager); testLayoutManager.expectLayouts(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { getActivity().getContainer().addView(recyclerView); } }); testLayoutManager.waitForLayout(2); assertEquals("item count in state should be correct", adapter.getItemCount() , itemCount.get()); assertEquals("structure changed should be true for first layout", true, structureChanged.get()); Thread.sleep(1000); //wait for other layouts. testLayoutManager.expectLayouts(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { recyclerView.requestLayout(); } }); testLayoutManager.waitForLayout(2); assertEquals("in second layout,structure changed should be false", false, structureChanged.get()); testLayoutManager.expectLayouts(1); // adapter.deleteAndNotify(3, 2); testLayoutManager.waitForLayout(2); assertEquals("when items are removed, item count in state should be updated", adapter.getItemCount(), itemCount.get()); assertEquals("structure changed should be true when items are removed", true, structureChanged.get()); testLayoutManager.expectLayouts(1); adapter.addAndNotify(2, 5); testLayoutManager.waitForLayout(2); assertEquals("when items are added, item count in state should be updated", adapter.getItemCount(), itemCount.get()); assertEquals("structure changed should be true when items are removed", true, structureChanged.get()); } @Test public void detachWithoutLayoutManager() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { try { setRecyclerView(recyclerView); removeRecyclerView(); } catch (Throwable t) { postExceptionToInstrumentation(t); } } }); checkForMainThreadException(); } @Test public void updateHiddenView() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); final int[] preLayoutRange = new int[]{0, 10}; final int[] postLayoutRange = new int[]{0, 10}; final AtomicBoolean enableGetViewTest = new AtomicBoolean(false); final List<Integer> disappearingPositions = new ArrayList<>(); final TestLayoutManager tlm = new TestLayoutManager() { @Override public boolean supportsPredictiveItemAnimations() { return true; } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { final int[] layoutRange = state.isPreLayout() ? preLayoutRange : postLayoutRange; detachAndScrapAttachedViews(recycler); layoutRange(recycler, layoutRange[0], layoutRange[1]); if (!state.isPreLayout()) { for (Integer position : disappearingPositions) { // test sanity. assertNull(findViewByPosition(position)); final View view = recycler.getViewForPosition(position); assertNotNull(view); addDisappearingView(view); measureChildWithMargins(view, 0, 0); // position item out of bounds. view.layout(0, -500, view.getMeasuredWidth(), -500 + view.getMeasuredHeight()); } } } catch (Throwable t) { postExceptionToInstrumentation(t); } layoutLatch.countDown(); } }; recyclerView.getItemAnimator().setMoveDuration(4000); recyclerView.getItemAnimator().setRemoveDuration(4000); final TestAdapter adapter = new TestAdapter(100); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); checkForMainThreadException(); // now, a child disappears disappearingPositions.add(0); // layout one shifted postLayoutRange[0] = 1; postLayoutRange[1] = 11; tlm.expectLayouts(2); adapter.addAndNotify(8, 1); tlm.waitForLayout(2); checkForMainThreadException(); tlm.expectLayouts(2); disappearingPositions.clear(); // now that item should be moving, invalidate it and delete it. enableGetViewTest.set(true); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { try { assertThat("test sanity, should still be animating", mRecyclerView.isAnimating(), CoreMatchers.is(true)); adapter.changeAndNotify(0, 1); adapter.deleteAndNotify(0, 1); } catch (Throwable throwable) { fail(throwable.getMessage()); } } }); tlm.waitForLayout(2); checkForMainThreadException(); } @Test public void focusBigViewOnTop() throws Throwable { focusTooBigViewTest(Gravity.TOP); } @Test public void focusBigViewOnLeft() throws Throwable { focusTooBigViewTest(Gravity.LEFT); } @Test public void focusBigViewOnRight() throws Throwable { focusTooBigViewTest(Gravity.RIGHT); } @Test public void focusBigViewOnBottom() throws Throwable { focusTooBigViewTest(Gravity.BOTTOM); } @Test public void focusBigViewOnLeftRTL() throws Throwable { focusTooBigViewTest(Gravity.LEFT, true); assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL, mRecyclerView.getLayoutManager().getLayoutDirection()); } @Test public void focusBigViewOnRightRTL() throws Throwable { focusTooBigViewTest(Gravity.RIGHT, true); assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL, mRecyclerView.getLayoutManager().getLayoutDirection()); } public void focusTooBigViewTest(final int gravity) throws Throwable { focusTooBigViewTest(gravity, false); } public void focusTooBigViewTest(final int gravity, final boolean rtl) throws Throwable { RecyclerView rv = new RecyclerView(getActivity()); if (rtl) { ViewCompat.setLayoutDirection(rv, ViewCompat.LAYOUT_DIRECTION_RTL); } final AtomicInteger vScrollDist = new AtomicInteger(0); final AtomicInteger hScrollDist = new AtomicInteger(0); final AtomicInteger vDesiredDist = new AtomicInteger(0); final AtomicInteger hDesiredDist = new AtomicInteger(0); TestLayoutManager tlm = new TestLayoutManager() { @Override public int getLayoutDirection() { return rtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR; } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); final View view = recycler.getViewForPosition(0); addView(view); int left = 0, top = 0; view.setBackgroundColor(Color.rgb(0, 0, 255)); switch (gravity) { case Gravity.LEFT: case Gravity.RIGHT: view.measure( View.MeasureSpec.makeMeasureSpec((int) (getWidth() * 1.5), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec((int) (getHeight() * .9), View.MeasureSpec.AT_MOST)); left = gravity == Gravity.LEFT ? getWidth() - view.getMeasuredWidth() - 80 : 90; top = 0; if (ViewCompat.LAYOUT_DIRECTION_RTL == getLayoutDirection()) { hDesiredDist.set((left + view.getMeasuredWidth()) - getWidth()); } else { hDesiredDist.set(left); } break; case Gravity.TOP: case Gravity.BOTTOM: view.measure( View.MeasureSpec.makeMeasureSpec((int) (getWidth() * .9), View.MeasureSpec.AT_MOST), View.MeasureSpec.makeMeasureSpec((int) (getHeight() * 1.5), View.MeasureSpec.EXACTLY)); top = gravity == Gravity.TOP ? getHeight() - view.getMeasuredHeight() - 80 : 90; left = 0; vDesiredDist.set(top); break; } view.layout(left, top, left + view.getMeasuredWidth(), top + view.getMeasuredHeight()); layoutLatch.countDown(); } @Override public boolean canScrollVertically() { return true; } @Override public boolean canScrollHorizontally() { return super.canScrollHorizontally(); } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { vScrollDist.addAndGet(dy); getChildAt(0).offsetTopAndBottom(-dy); return dy; } @Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { hScrollDist.addAndGet(dx); getChildAt(0).offsetLeftAndRight(-dx); return dx; } }; TestAdapter adapter = new TestAdapter(10); rv.setAdapter(adapter); rv.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(rv); tlm.waitForLayout(2); View view = rv.getChildAt(0); assertTrue("test sanity", requestFocus(view, true)); assertTrue("test sanity", view.hasFocus()); assertEquals(vDesiredDist.get(), vScrollDist.get()); assertEquals(hDesiredDist.get(), hScrollDist.get()); assertEquals(mRecyclerView.getPaddingTop(), view.getTop()); if (rtl) { assertEquals(mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(), view.getRight()); } else { assertEquals(mRecyclerView.getPaddingLeft(), view.getLeft()); } } @Test public void firstLayoutWithAdapterChanges() throws Throwable { final TestAdapter adapter = new TestAdapter(0); final RecyclerView rv = new RecyclerView(getActivity()); setVisibility(rv, View.GONE); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { super.onLayoutChildren(recycler, state); layoutRange(recycler, 0, state.getItemCount()); } catch (Throwable t) { postExceptionToInstrumentation(t); } finally { layoutLatch.countDown(); } } @Override public boolean supportsPredictiveItemAnimations() { return true; } }; rv.setLayoutManager(tlm); rv.setAdapter(adapter); rv.setHasFixedSize(true); setRecyclerView(rv); tlm.expectLayouts(1); tlm.assertNoLayout("test sanity, layout should not run", 1); getInstrumentation().waitForIdleSync(); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { try { adapter.addAndNotify(2); } catch (Throwable throwable) { throwable.printStackTrace(); } rv.setVisibility(View.VISIBLE); } }); checkForMainThreadException(); tlm.waitForLayout(2); assertEquals(2, rv.getChildCount()); checkForMainThreadException(); } @Test public void computeScrollOffsetWithoutLayoutManager() throws Throwable { RecyclerView rv = new RecyclerView(getActivity()); rv.setAdapter(new TestAdapter(10)); setRecyclerView(rv); assertEquals(0, rv.computeHorizontalScrollExtent()); assertEquals(0, rv.computeHorizontalScrollOffset()); assertEquals(0, rv.computeHorizontalScrollRange()); assertEquals(0, rv.computeVerticalScrollExtent()); assertEquals(0, rv.computeVerticalScrollOffset()); assertEquals(0, rv.computeVerticalScrollRange()); } @Test public void computeScrollOffsetWithoutAdapter() throws Throwable { RecyclerView rv = new RecyclerView(getActivity()); rv.setLayoutManager(new TestLayoutManager()); setRecyclerView(rv); assertEquals(0, rv.computeHorizontalScrollExtent()); assertEquals(0, rv.computeHorizontalScrollOffset()); assertEquals(0, rv.computeHorizontalScrollRange()); assertEquals(0, rv.computeVerticalScrollExtent()); assertEquals(0, rv.computeVerticalScrollOffset()); assertEquals(0, rv.computeVerticalScrollRange()); } @Test public void focusRectOnScreenWithDecorOffsets() throws Throwable { focusRectOnScreenTest(true); } @Test public void focusRectOnScreenWithout() throws Throwable { focusRectOnScreenTest(false); } public void focusRectOnScreenTest(boolean addItemDecors) throws Throwable { RecyclerView rv = new RecyclerView(getActivity()); final AtomicInteger scrollDist = new AtomicInteger(0); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); final View view = recycler.getViewForPosition(0); addView(view); measureChildWithMargins(view, 0, 0); view.layout(0, -20, view.getWidth(), -20 + view.getHeight());// ignore decors on purpose layoutLatch.countDown(); } @Override public boolean canScrollVertically() { return true; } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { scrollDist.addAndGet(dy); return dy; } }; TestAdapter adapter = new TestAdapter(10); if (addItemDecors) { rv.addItemDecoration(new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { outRect.set(0, 10, 0, 10); } }); } rv.setAdapter(adapter); rv.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(rv); tlm.waitForLayout(2); View view = rv.getChildAt(0); requestFocus(view, true); assertEquals(addItemDecors ? -30 : -20, scrollDist.get()); } @Test public void unimplementedSmoothScroll() throws Throwable { final AtomicInteger receivedScrollToPosition = new AtomicInteger(-1); final AtomicInteger receivedSmoothScrollToPosition = new AtomicInteger(-1); final CountDownLatch cbLatch = new CountDownLatch(2); TestLayoutManager tlm = new TestLayoutManager() { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, 10); layoutLatch.countDown(); } @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { assertEquals(-1, receivedSmoothScrollToPosition.get()); receivedSmoothScrollToPosition.set(position); RecyclerView.SmoothScroller ss = new LinearSmoothScroller(recyclerView.getContext()) { @Override public PointF computeScrollVectorForPosition(int targetPosition) { return null; } }; ss.setTargetPosition(position); startSmoothScroll(ss); cbLatch.countDown(); } @Override public void scrollToPosition(int position) { assertEquals(-1, receivedScrollToPosition.get()); receivedScrollToPosition.set(position); cbLatch.countDown(); } }; RecyclerView rv = new RecyclerView(getActivity()); rv.setAdapter(new TestAdapter(100)); rv.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(rv); tlm.waitForLayout(2); freezeLayout(true); smoothScrollToPosition(35, false); assertEquals("smoothScrollToPosition should be ignored when frozen", -1, receivedSmoothScrollToPosition.get()); freezeLayout(false); smoothScrollToPosition(35, false); assertTrue("both scrolls should be called", cbLatch.await(3, TimeUnit.SECONDS)); checkForMainThreadException(); assertEquals(35, receivedSmoothScrollToPosition.get()); assertEquals(35, receivedScrollToPosition.get()); } @Test public void jumpingJackSmoothScroller() throws Throwable { jumpingJackSmoothScrollerTest(true); } @Test public void jumpingJackSmoothScrollerGoesIdle() throws Throwable { jumpingJackSmoothScrollerTest(false); } @Test public void testScrollByBeforeFirstLayout() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); TestAdapter adapter = new TestAdapter(10); recyclerView.setLayoutManager(new TestLayoutManager() { AtomicBoolean didLayout = new AtomicBoolean(false); @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); didLayout.set(true); } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { assertThat("should run layout before scroll", didLayout.get(), CoreMatchers.is(true)); return super.scrollVerticallyBy(dy, recycler, state); } @Override public boolean canScrollVertically() { return true; } }); recyclerView.setAdapter(adapter); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { try { setRecyclerView(recyclerView); recyclerView.scrollBy(10, 19); } catch (Throwable throwable) { postExceptionToInstrumentation(throwable); } } }); checkForMainThreadException(); } private void jumpingJackSmoothScrollerTest(final boolean succeed) throws Throwable { final List<Integer> receivedScrollToPositions = new ArrayList<>(); final TestAdapter testAdapter = new TestAdapter(200); final AtomicBoolean mTargetFound = new AtomicBoolean(false); TestLayoutManager tlm = new TestLayoutManager() { int pendingScrollPosition = -1; @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); final int pos = pendingScrollPosition < 0 ? 0: pendingScrollPosition; layoutRange(recycler, pos, pos + 10); if (layoutLatch != null) { layoutLatch.countDown(); } } @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, final int position) { RecyclerView.SmoothScroller ss = new LinearSmoothScroller(recyclerView.getContext()) { @Override public PointF computeScrollVectorForPosition(int targetPosition) { return new PointF(0, 1); } @Override protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { super.onTargetFound(targetView, state, action); mTargetFound.set(true); } @Override protected void updateActionForInterimTarget(Action action) { int limit = succeed ? getTargetPosition() : 100; if (pendingScrollPosition + 2 < limit) { if (pendingScrollPosition != NO_POSITION) { assertEquals(pendingScrollPosition, getChildViewHolderInt(getChildAt(0)) .getAdapterPosition()); } action.jumpTo(pendingScrollPosition + 2); } } }; ss.setTargetPosition(position); startSmoothScroll(ss); } @Override public void scrollToPosition(int position) { receivedScrollToPositions.add(position); pendingScrollPosition = position; requestLayout(); } }; final RecyclerView rv = new RecyclerView(getActivity()); rv.setAdapter(testAdapter); rv.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(rv); tlm.waitForLayout(2); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { rv.smoothScrollToPosition(150); } }); int limit = 100; while (rv.getLayoutManager().isSmoothScrolling() && --limit > 0) { Thread.sleep(200); checkForMainThreadException(); } checkForMainThreadException(); assertTrue(limit > 0); for (int i = 1; i < 100; i+=2) { assertTrue("scroll positions must include " + i, receivedScrollToPositions.contains(i)); } assertEquals(succeed, mTargetFound.get()); } private static class TestViewHolder2 extends RecyclerView.ViewHolder { Object mData; public TestViewHolder2(View itemView) { super(itemView); } } private static class TestAdapter2 extends RecyclerView.Adapter<TestViewHolder2> { List<Item> mItems; private TestAdapter2(int count) { mItems = new ArrayList<>(count); for (int i = 0; i < count; i++) { mItems.add(new Item(i, "Item " + i)); } } @Override public TestViewHolder2 onCreateViewHolder(ViewGroup parent, int viewType) { return new TestViewHolder2(new TextView(parent.getContext())); } @Override public void onBindViewHolder(TestViewHolder2 holder, int position) { final Item item = mItems.get(position); ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")"); } @Override public int getItemCount() { return mItems.size(); } } public interface AdapterRunnable { void run(TestAdapter adapter) throws Throwable; } public class LayoutAllLayoutManager extends TestLayoutManager { private final boolean mAllowNullLayoutLatch; public LayoutAllLayoutManager() { // by default, we don't allow unexpected layouts. this(false); } public LayoutAllLayoutManager(boolean allowNullLayoutLatch) { mAllowNullLayoutLatch = allowNullLayoutLatch; } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); layoutRange(recycler, 0, state.getItemCount()); if (!mAllowNullLayoutLatch || layoutLatch != null) { layoutLatch.countDown(); } } } /** * Proxy class to make protected methods public */ public static class TestRecyclerView extends RecyclerView { public TestRecyclerView(Context context) { super(context); } public TestRecyclerView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public TestRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public void detachViewFromParent(int index) { super.detachViewFromParent(index); } @Override public void attachViewToParent(View child, int index, ViewGroup.LayoutParams params) { super.attachViewToParent(child, index, params); } } private interface ViewRunnable { void run(View view) throws RuntimeException; } }