/* * 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 org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import android.support.test.InstrumentationRegistry; import android.graphics.Color; import android.graphics.PointF; import android.graphics.Rect; import android.os.SystemClock; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.test.TouchUtils; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.TextView; 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; import static android.support.v7.widget.RecyclerView.NO_POSITION; import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE; import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING; import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING; import static android.support.v7.widget.RecyclerView.getChildViewHolderInt; import android.support.test.runner.AndroidJUnit4; @RunWith(AndroidJUnit4.class) 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 = true; private static final String TAG = "RecyclerViewLayoutTest"; public RecyclerViewLayoutTest() { super(DEBUG); } @Before @Override public void setUp() throws Exception { super.setUp(); injectInstrumentation(InstrumentationRegistry.getInstrumentation()); } @After @Override public void tearDown() throws Exception { super.tearDown(); } @Test public void testFlingFrozen() throws Throwable { testScrollFrozen(true); } @Test public void testDragFrozen() throws Throwable { testScrollFrozen(false); } 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(this, 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(this, 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 testFocusSearchFailFrozen() 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); final View c = recyclerView.getChildAt(recyclerView.getChildCount() - 1); runTestOnUiThread(new Runnable() { @Override public void run() { c.requestFocus(); } }); assertTrue(c.hasFocus()); freezeLayout(true); sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); assertEquals("onFocusSearchFailed should not be called when layout is frozen", 0, focusSearchCalled.get()); freezeLayout(false); sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); assertEquals(1, focusSearchCalled.get()); } @Test public void testFrozenAndChangeAdapter() 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 testScrollToPositionCallback() 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.getViewTreeObserver().addOnScrollChangedListener( new ViewTreeObserver.OnScrollChangedListener() { @Override public void onScrollChanged() { viewGroupCounter.incrementAndGet(); } }); recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { rvCounter.incrementAndGet(); super.onScrolled(recyclerView, dx, dy); } }); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(2); // wait for draw :/ Thread.sleep(1000); 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 testScrollInBothDirectionEqual() throws Throwable { scrollInBothDirection(3, 3, 1000, 1000); } @Test public void testScrollInBothDirectionMoreVertical() throws Throwable { scrollInBothDirection(2, 3, 1000, 1000); } @Test public void testScrollInBothDirectionMoreHorizontal() throws Throwable { scrollInBothDirection(3, 2, 1000, 1000); } @Test public void testScrollHorizontalOnly() throws Throwable { scrollInBothDirection(3, 0, 1000, 0); } @Test public void testScrollVerticalOnly() throws Throwable { scrollInBothDirection(0, 3, 0, 1000); } @Test public void testScrollInBothDirectionEqualReverse() throws Throwable { scrollInBothDirection(3, 3, -1000, -1000); } @Test public void testScrollInBothDirectionMoreVerticalReverse() throws Throwable { scrollInBothDirection(2, 3, -1000, -1000); } @Test public void testScrollInBothDirectionMoreHorizontalReverse() throws Throwable { scrollInBothDirection(3, 2, -1000, -1000); } @Test public void testScrollHorizontalOnlyReverse() throws Throwable { scrollInBothDirection(3, 0, -1000, 0); } @Test public void testScrollVerticalOnlyReverse() 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 testDragHorizontal() throws Throwable { scrollInOtherOrientationTest(FLAG_HORIZONTAL); } @Test public void testDragVertical() throws Throwable { scrollInOtherOrientationTest(FLAG_VERTICAL); } @Test public void testFlingHorizontal() throws Throwable { scrollInOtherOrientationTest(FLAG_HORIZONTAL | FLAG_FLING); } @Test public void testFlingVertical() throws Throwable { scrollInOtherOrientationTest(FLAG_VERTICAL | FLAG_FLING); } @Test public void testNestedDragVertical() throws Throwable { TestedFrameLayout tfl = getActivity().mContainer; tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); scrollInOtherOrientationTest(FLAG_VERTICAL, 0); } @Test public void testNestedDragHorizontal() throws Throwable { TestedFrameLayout tfl = getActivity().mContainer; tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0); } @Test public void testNestedDragHorizontalCallsStopNestedScroll() throws Throwable { TestedFrameLayout tfl = getActivity().mContainer; 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 testNestedDragVerticalCallsStopNestedScroll() throws Throwable { TestedFrameLayout tfl = getActivity().mContainer; 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(this, 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); runTestOnUiThread(new Runnable() { @Override public void run() { boolean result = mRecyclerView.fling(velocityX, velocityY); didStart.set(result); } }); if (!didStart.get()) { return false; } // cannot set scroll listener in case it is subject to some test so instead doing a busy // loop until state goes idle while (mRecyclerView.getScrollState() != SCROLL_STATE_IDLE) { getInstrumentation().waitForIdleSync(); } return true; } private void assertPendingUpdatesAndLayout(TestLayoutManager testLayoutManager, final Runnable runnable) throws Throwable { testLayoutManager.expectLayouts(1); runTestOnUiThread(new Runnable() { @Override public void run() { runnable.run(); assertTrue(mRecyclerView.hasPendingAdapterUpdates()); } }); testLayoutManager.waitForLayout(1); assertFalse(mRecyclerView.hasPendingAdapterUpdates()); } 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); } } @Test public void testHasPendingUpdatesBeforeFirstLayout() 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 testNoPendingUpdatesAfterLayout() 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 testHasPendingUpdatesWhenAdapterIsChanged() throws Throwable { RecyclerView recyclerView = new RecyclerView(getActivity()); TestLayoutManager layoutManager = new DumbLayoutManager(); final TestAdapter testAdapter = new TestAdapter(10); setupBasic(recyclerView, layoutManager, testAdapter, false); assertPendingUpdatesAndLayout(layoutManager, new Runnable() { @Override public void run() { testAdapter.notifyItemRemoved(1); } }); assertPendingUpdatesAndLayout(layoutManager, new Runnable() { @Override public void run() { testAdapter.notifyItemInserted(2); } }); assertPendingUpdatesAndLayout(layoutManager, new Runnable() { @Override public void run() { testAdapter.notifyItemMoved(2, 3); } }); assertPendingUpdatesAndLayout(layoutManager, new Runnable() { @Override public void run() { testAdapter.notifyItemChanged(2); } }); assertPendingUpdatesAndLayout(layoutManager, new Runnable() { @Override public void run() { testAdapter.notifyDataSetChanged(); } }); } @Test public void testTransientStateRecycleViaAdapter() throws Throwable { transientStateRecycleTest(true, false); } @Test public void testTransientStateRecycleViaTransientStateCleanup() throws Throwable { transientStateRecycleTest(false, true); } @Test public void testTransientStateDontRecycle() throws Throwable { transientStateRecycleTest(false, false); } public void transientStateRecycleTest(final boolean succeed, final boolean unsetTransientState) throws Throwable { final List failedToRecycle = new ArrayList(); final List 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 testAdapterPositionInvalidation() 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); runTestOnUiThread(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 testAdapterPositionsBasic() throws Throwable { adapterPositionsTest(null); } @Test public void testAdapterPositionsRemoveItems() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.deleteAndNotify(3, 4); } }); } @Test public void testAdapterPositionsRemoveItemsBefore() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.deleteAndNotify(0, 1); } }); } @Test public void testAdapterPositionsAddItemsBefore() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.addAndNotify(0, 5); } }); } @Test public void testAdapterPositionsAddItemsInside() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.addAndNotify(3, 2); } }); } @Test public void testAdapterPositionsMoveItems() throws Throwable { adapterPositionsTest(new AdapterRunnable() { @Override public void run(TestAdapter adapter) throws Throwable { adapter.moveAndNotify(3, 5); } }); } @Test public void testAdapterPositionsNotifyDataSetChanged() 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(); } }); } @Test public void testAvoidLeakingRecyclerViewIfViewIsNotRecycled() throws Throwable { final AtomicBoolean failedToRecycle = new AtomicBoolean(false); 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; } }; rv.setAdapter(adapter); rv.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(rv); tlm.waitForLayout(1); final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0)); runTestOnUiThread(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)); assertTrue(failedToRecycle.get()); assertNull(vh.mOwnerRecyclerView); checkForMainThreadException(); } @Test public void testAvoidLeakingRecyclerViewViaViewHolder() 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(); } 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); runTestOnUiThread(new Runnable() { @Override public void run() { try { final int count = recyclerView.getChildCount(); Map 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 testScrollStateForSmoothScroll() 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.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { stateCnts[newState] = stateCnts[newState] + 1; latch.countDown(); } }); runTestOnUiThread(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 testScrollStateForSmoothScrollWithStop() 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.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { stateCnts[newState] = stateCnts[newState] + 1; latch.countDown(); } }); runTestOnUiThread(new Runnable() { @Override public void run() { mRecyclerView.smoothScrollBy(0, 500); } }); latch.await(5, TimeUnit.SECONDS); runTestOnUiThread(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 testScrollStateForFling() 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.setOnScrollListener(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; runTestOnUiThread(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 testScrollStateForFlingWithStop() 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.setOnScrollListener(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; runTestOnUiThread(new Runnable() { @Override public void run() { mRecyclerView.fling(0, Math.round(fling)); } }); latch.await(5, TimeUnit.SECONDS); runTestOnUiThread(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 testScrollStateDrag() 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.setOnScrollListener(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 { runTestOnUiThread(new Runnable() { @Override public void run() { if (view.onInterceptTouchEvent(event)) { view.onTouchEvent(event); } } }); } @Test public void testRecycleScrap() 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) { 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 (state.mOldChangedHolders != null) { for (int i = state.mOldChangedHolders.size() - 1; i >= 0; i--) { if (useRecycler) { recycler.recycleView( state.mOldChangedHolders.valueAt(i).itemView); } else { removeAndRecycleView( state.mOldChangedHolders.valueAt(i).itemView, recycler); } } } assertEquals("no scrap should be left over", 0, recycler.getScrapCount()); assertEquals("pre layout map should be empty", 0, state.mPreLayoutHolderMap.size()); assertEquals("post layout map should be empty", 0, state.mPostLayoutHolderMap.size()); if (state.mOldChangedHolders != null) { assertEquals("post old change map should be empty", 0, state.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); 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 testAccessRecyclerOnOnMeasure() throws Throwable { accessRecyclerOnOnMeasureTest(false); removeRecyclerView(); accessRecyclerOnOnMeasureTest(true); } @Test public void testSmoothScrollWithRemovedItemsAndRemoveItem() throws Throwable { smoothScrollTest(true); } @Test public void testSmoothScrollWithRemovedItems() 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); runTestOnUiThread(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); runTestOnUiThread(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 testConsecutiveSmoothScroll() 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); runTestOnUiThread(new Runnable() { @Override public void run() { rv.smoothScrollBy(0, 2000); } }); Thread.sleep(250); final AtomicInteger scrollAmt = new AtomicInteger(); runTestOnUiThread(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)); } 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 testSetCompatibleAdapter() 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 testSetIncompatibleAdapter() 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 testRecycleIgnored() 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); runTestOnUiThread(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 testFindIgnoredByPosition() 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]; runTestOnUiThread(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 testInvalidateAllDecorOffsets() 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 { runTestOnUiThread(new Runnable() { @Override public void run() { recyclerView.addItemDecoration(itemDecoration); } }); } public void removeItemDecoration(final RecyclerView recyclerView, final RecyclerView.ItemDecoration itemDecoration) throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { recyclerView.removeItemDecoration(itemDecoration); } }); } public void invalidateDecorOffsets(final RecyclerView recyclerView) throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { recyclerView.invalidateItemDecorations(); } }); } @Test public void testInvalidateDecorOffsets() throws Throwable { final TestAdapter adapter = new TestAdapter(10); adapter.setHasStableIds(true); final RecyclerView recyclerView = new RecyclerView(getActivity()); recyclerView.setAdapter(adapter); final Map 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( "Decord insets validation for VH should have expected value.", changes.get(vh.getItemId()).booleanValue(), 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 i = 0; i < changedItems.length; i++) { changes.put(mRecyclerView.findViewHolderForLayoutPosition(changedItems[i]).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 testMovingViaStableIds() 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); runTestOnUiThread(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 testAdapterChangeDuringLayout() 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 { 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, mainThreadException instanceof IllegalStateException); mainThreadException = null; } @Test public void testAdapterChangeDuringScroll() 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 { 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, mainThreadException instanceof IllegalStateException); mainThreadException = null; } @Test public void testRecycleOnDetach() 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 testUpdatesWhileDetached() 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 testUpdatesAfterDetach() 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(); runTestOnUiThread(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 testNotifyDataSetChangedWithStableIds() throws Throwable { final int defaultViewType = 1; final Map viewTypeMap = new HashMap(); final Map oldPositionToNewPositionMapping = new HashMap(); final TestAdapter adapter = new TestAdapter(100) { @Override public int getItemViewType(int position) { Integer type = viewTypeMap.get(mItems.get(position)); return type == null ? defaultViewType : type; } @Override public long getItemId(int position) { return mItems.get(position).mId; } }; adapter.setHasStableIds(true); final ArrayList 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); runTestOnUiThread(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 testCallbacksDuringAdapterSwap() throws Throwable { callbacksDuringAdapterChange(true); } @Test public void testCallbacksDuringAdapterSet() 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 testFindViewById() 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 testTypeForCache() 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); runTestOnUiThread(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 testTypeForExistingViews() 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 testState() 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); runTestOnUiThread(new Runnable() { @Override public void run() { getActivity().mContainer.addView(recyclerView); } }); testLayoutManager.waitForLayout(2, TimeUnit.SECONDS); 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); runTestOnUiThread(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 testDetachWithoutLayoutManager() throws Throwable { final RecyclerView recyclerView = new RecyclerView(getActivity()); runTestOnUiThread(new Runnable() { @Override public void run() { try { setRecyclerView(recyclerView); removeRecyclerView(); } catch (Throwable t) { postExceptionToInstrumentation(t); } } }); checkForMainThreadException(); } @Test public void testUpdateHiddenView() throws Throwable { final RecyclerView.ViewHolder[] mTargetVH = new RecyclerView.ViewHolder[1]; 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 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); 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(2000); recyclerView.getItemAnimator().setRemoveDuration(2000); final TestAdapter adapter = new TestAdapter(100); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(tlm); tlm.expectLayouts(1); setRecyclerView(recyclerView); tlm.waitForLayout(1); checkForMainThreadException(); mTargetVH[0] = recyclerView.findViewHolderForAdapterPosition(0); // 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); runTestOnUiThread(new Runnable() { @Override public void run() { try { adapter.changeAndNotify(0, 1); adapter.deleteAndNotify(0, 1); } catch (Throwable throwable) { throwable.printStackTrace(); } } }); tlm.waitForLayout(2); checkForMainThreadException(); } @Test public void testFocusBigViewOnTop() throws Throwable { focusTooBigViewTest(Gravity.TOP); } @Test public void testFocusBigViewOnLeft() throws Throwable { focusTooBigViewTest(Gravity.LEFT); } @Test public void testFocusBigViewOnRight() throws Throwable { focusTooBigViewTest(Gravity.RIGHT); } @Test public void testFocusBigViewOnBottom() throws Throwable { focusTooBigViewTest(Gravity.BOTTOM); } @Test public void testFocusBigViewOnLeftRTL() throws Throwable { focusTooBigViewTest(Gravity.LEFT, true); assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL, mRecyclerView.getLayoutManager().getLayoutDirection()); } @Test public void testFocusBigViewOnRightRTL() 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); requestFocus(view); Thread.sleep(1000); 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 testFocusRectOnScreenWithDecorOffsets() throws Throwable { focusRectOnScreenTest(true); } @Test public void testFocusRectOnScreenWithout() 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); Thread.sleep(1000); assertEquals(addItemDecors ? -30 : -20, scrollDist.get()); } @Test public void testUnimplementedSmoothScroll() 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); assertEquals("smoothScrollToPosition should be ignored when frozen", -1, receivedSmoothScrollToPosition.get()); freezeLayout(false); smoothScrollToPosition(35); assertTrue("both scrolls should be called", cbLatch.await(3, TimeUnit.SECONDS)); checkForMainThreadException(); assertEquals(35, receivedSmoothScrollToPosition.get()); assertEquals(35, receivedScrollToPosition.get()); } @Test public void testJumpingJackSmoothScroller() throws Throwable { jumpingJackSmoothScrollerTest(true); } @Test public void testJumpingJackSmoothScrollerGoesIdle() throws Throwable { jumpingJackSmoothScrollerTest(false); } private void jumpingJackSmoothScrollerTest(final boolean succeed) throws Throwable { final List 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); runTestOnUiThread(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 { List 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(); } } private static interface AdapterRunnable { public void run(TestAdapter adapter) throws Throwable; } }