blob: 2e430e567504bccf43da187695674cb17ac5974d [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 android.graphics.PointF;
import android.os.Debug;
import android.os.SystemClock;
import android.support.v4.view.ViewCompat;
import android.test.TouchUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.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.SCROLL_STATE_IDLE;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING;
public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest {
private static final boolean DEBUG = false;
private static final String TAG = "RecyclerViewLayoutTest";
public RecyclerViewLayoutTest() {
super(DEBUG);
}
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);
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());
}
public void testScrollInBothDirectionEqual() throws Throwable {
scrollInBothDirection(3, 3, 1000, 1000);
}
public void testScrollInBothDirectionMoreVertical() throws Throwable {
scrollInBothDirection(2, 3, 1000, 1000);
}
public void testScrollInBothDirectionMoreHorizontal() throws Throwable {
scrollInBothDirection(3, 2, 1000, 1000);
}
public void testScrollHorizontalOnly() throws Throwable {
scrollInBothDirection(3, 0, 1000, 0);
}
public void testScrollVerticalOnly() throws Throwable {
scrollInBothDirection(0, 3, 0, 1000);
}
public void testScrollInBothDirectionEqualReverse() throws Throwable {
scrollInBothDirection(3, 3, -1000, -1000);
}
public void testScrollInBothDirectionMoreVerticalReverse() throws Throwable {
scrollInBothDirection(2, 3, -1000, -1000);
}
public void testScrollInBothDirectionMoreHorizontalReverse() throws Throwable {
scrollInBothDirection(3, 2, -1000, -1000);
}
public void testScrollHorizontalOnlyReverse() throws Throwable {
scrollInBothDirection(3, 0, -1000, 0);
}
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());
}
public void testDraglHorizontal() throws Throwable {
scrollInOtherOrientationTest(true, true);
}
public void testDragVertical() throws Throwable {
scrollInOtherOrientationTest(false, true);
}
public void testFlingHorizontal() throws Throwable {
scrollInOtherOrientationTest(true, false);
}
public void testFlingVertical() throws Throwable {
scrollInOtherOrientationTest(false, false);
}
public void scrollInOtherOrientationTest(final boolean horizontal, final boolean drag)
throws Throwable {
RecyclerView recyclerView = new RecyclerView(getActivity());
final AtomicBoolean scrolledHorizontal = new AtomicBoolean(false);
final AtomicBoolean scrolledVertical = new AtomicBoolean(false);
TestLayoutManager tlm = new TestLayoutManager() {
@Override
public boolean canScrollHorizontally() {
return horizontal;
}
@Override
public boolean canScrollVertically() {
return !horizontal;
}
@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 dy;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
scrolledHorizontal.set(true);
return dx;
}
};
TestAdapter adapter = new TestAdapter(100);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(tlm);
tlm.expectLayouts(1);
setRecyclerView(recyclerView);
tlm.waitForLayout(2);
if (drag) {
TouchUtils.dragViewTo(this, mRecyclerView, Gravity.LEFT | Gravity.TOP, 200, 200);
} else {// fling
assertTrue("test sanity, fling must run", fling(600, 600));
}
assertEquals("horizontal scroll", horizontal, scrolledHorizontal.get());
assertEquals("vertical scroll",!horizontal, 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;
}
public void testTransientStateRecycleViaAdapter() throws Throwable {
transientStateRecycleTest(true, false);
}
public void testTransientStateRecycleViaTransientStateCleanup() throws Throwable {
transientStateRecycleTest(false, true);
}
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));
}
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);
}
}
});
}
public void testAdapterPositionsBasic() throws Throwable {
adapterPositionsTest(null);
}
public void testAdapterPositionsRemoveItems() throws Throwable {
adapterPositionsTest(new AdapterRunnable() {
@Override
public void run(TestAdapter adapter) throws Throwable {
adapter.deleteAndNotify(3, 4);
}
});
}
public void testAdapterPositionsRemoveItemsBefore() throws Throwable {
adapterPositionsTest(new AdapterRunnable() {
@Override
public void run(TestAdapter adapter) throws Throwable {
adapter.deleteAndNotify(0, 1);
}
});
}
public void testAdapterPositionsAddItemsBefore() throws Throwable {
adapterPositionsTest(new AdapterRunnable() {
@Override
public void run(TestAdapter adapter) throws Throwable {
adapter.addAndNotify(0, 5);
}
});
}
public void testAdapterPositionsAddItemsInside() throws Throwable {
adapterPositionsTest(new AdapterRunnable() {
@Override
public void run(TestAdapter adapter) throws Throwable {
adapter.addAndNotify(3, 2);
}
});
}
public void testAdapterPositionsMoveItems() throws Throwable {
adapterPositionsTest(new AdapterRunnable() {
@Override
public void run(TestAdapter adapter) throws Throwable {
adapter.moveAndNotify(3, 5);
}
});
}
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();
}
});
}
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();
}
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]);
}
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]);
}
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]);
}
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]);
}
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);
}
}
});
}
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();
}
public void testAccessRecyclerOnOnMeasure() throws Throwable {
accessRecyclerOnOnMeasureTest(false);
removeRecyclerView();
accessRecyclerOnOnMeasureTest(true);
}
public void testSmoothScrollWithRemovedItems() throws Throwable {
smoothScrollTest(false);
removeRecyclerView();
smoothScrollTest(true);
}
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();
}
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();
}
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());
}
}
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();
}
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());
}
});
}
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]);
}
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();
}
});
}
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();
}
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();
}
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;
}
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;
}
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);
}
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);
}
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());
}
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();
}
public void testCallbacksDuringAdapterSwap() throws Throwable {
callbacksDuringAdapterChange(true);
}
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;
}
};
}
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);
}
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());
}
}
});
}
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();
}
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());
}
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();
}
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;
}
}