blob: 61312d322224a534ff9a7e6471517a9f8c21ed55 [file] [log] [blame]
/*
* 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 = false;
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 CountDownLatch focusLatch = new CountDownLatch(1);
final AtomicInteger focusSearchCalled = new AtomicInteger(0);
TestLayoutManager tlm = new TestLayoutManager() {
@Override
public boolean canScrollHorizontally() {
return true;
}
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
layoutRange(recycler, 0, 10);
layoutLatch.countDown();
}
@Override
public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler,
RecyclerView.State state) {
focusSearchCalled.addAndGet(1);
focusLatch.countDown();
return null;
}
};
TestAdapter adapter = new TestAdapter(100);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(tlm);
tlm.expectLayouts(1);
setRecyclerView(recyclerView);
tlm.waitForLayout(2);
final View c = recyclerView.getChildAt(recyclerView.getChildCount() - 1);
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);
assertTrue(focusLatch.await(2, TimeUnit.SECONDS));
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<View> failedToRecycle = new ArrayList<View>();
final List<View> recycled = new ArrayList<View>();
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<View, Integer> layoutPositions = new HashMap<View, Integer>();
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<Long, Boolean> changes = new HashMap<Long, Boolean>();
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<Item, Integer> viewTypeMap = new HashMap<Item, Integer>();
final Map<Integer, Integer> oldPositionToNewPositionMapping =
new HashMap<Integer, Integer>();
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<Item> previousItems = new ArrayList<Item>();
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<Integer> disappearingPositions = new ArrayList<Integer>();
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<Integer> receivedScrollToPositions = new ArrayList<>();
final TestAdapter testAdapter = new TestAdapter(200);
final AtomicBoolean mTargetFound = new AtomicBoolean(false);
TestLayoutManager tlm = new TestLayoutManager() {
int pendingScrollPosition = -1;
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
final int pos = pendingScrollPosition < 0 ? 0: pendingScrollPosition;
layoutRange(recycler, pos, pos + 10);
if (layoutLatch != null) {
layoutLatch.countDown();
}
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
final int position) {
RecyclerView.SmoothScroller ss =
new LinearSmoothScroller(recyclerView.getContext()) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return new PointF(0, 1);
}
@Override
protected void onTargetFound(View targetView, RecyclerView.State state,
Action action) {
super.onTargetFound(targetView, state, action);
mTargetFound.set(true);
}
@Override
protected void updateActionForInterimTarget(Action action) {
int limit = succeed ? getTargetPosition() : 100;
if (pendingScrollPosition + 2 < limit) {
if (pendingScrollPosition != NO_POSITION) {
assertEquals(pendingScrollPosition,
getChildViewHolderInt(getChildAt(0))
.getAdapterPosition());
}
action.jumpTo(pendingScrollPosition + 2);
}
}
};
ss.setTargetPosition(position);
startSmoothScroll(ss);
}
@Override
public void scrollToPosition(int position) {
receivedScrollToPositions.add(position);
pendingScrollPosition = position;
requestLayout();
}
};
final RecyclerView rv = new RecyclerView(getActivity());
rv.setAdapter(testAdapter);
rv.setLayoutManager(tlm);
tlm.expectLayouts(1);
setRecyclerView(rv);
tlm.waitForLayout(2);
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<TestViewHolder2> {
List<Item> mItems;
private TestAdapter2(int count) {
mItems = new ArrayList<Item>(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;
}
}