blob: f8d11d8995c183c9f4e3547854b835485c71a6e1 [file] [log] [blame]
/*
* Copyright (C) 2015 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.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import android.graphics.Rect;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.view.ViewCompat;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.Log;
import android.view.View;
import android.view.ViewParent;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static android.support.v7.widget.LayoutState.LAYOUT_START;
import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@RunWith(Parameterized.class)
@MediumTest
public class StaggeredGridLayoutManagerBaseConfigSetTest
extends BaseStaggeredGridLayoutManagerTest {
@Parameterized.Parameters(name = "{0}")
public static List<Config> getParams() {
return createBaseVariations();
}
private final Config mConfig;
public StaggeredGridLayoutManagerBaseConfigSetTest(Config config)
throws CloneNotSupportedException {
mConfig = (Config) config.clone();
}
@Test
public void rTL() throws Throwable {
rtlTest(false, false);
}
@Test
public void rTLChangeAfter() throws Throwable {
rtlTest(true, false);
}
@Test
public void rTLItemWrapContent() throws Throwable {
rtlTest(false, true);
}
@Test
public void rTLChangeAfterItemWrapContent() throws Throwable {
rtlTest(true, true);
}
void rtlTest(boolean changeRtlAfter, final boolean wrapContent) throws Throwable {
if (mConfig.mSpanCount == 1) {
mConfig.mSpanCount = 2;
}
String logPrefix = mConfig + ", changeRtlAfterLayout:" + changeRtlAfter;
setupByConfig(mConfig.itemCount(5),
new GridTestAdapter(mConfig.mItemCount, mConfig.mOrientation) {
@Override
public void onBindViewHolder(TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
if (wrapContent) {
if (mOrientation == HORIZONTAL) {
holder.itemView.getLayoutParams().height
= RecyclerView.LayoutParams.WRAP_CONTENT;
} else {
holder.itemView.getLayoutParams().width
= RecyclerView.LayoutParams.MATCH_PARENT;
}
}
}
});
if (changeRtlAfter) {
waitFirstLayout();
mLayoutManager.expectLayouts(1);
mLayoutManager.setFakeRtl(true);
mLayoutManager.waitForLayout(2);
} else {
mLayoutManager.mFakeRTL = true;
waitFirstLayout();
}
assertEquals("view should become rtl", true, mLayoutManager.isLayoutRTL());
OrientationHelper helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
View child0 = mLayoutManager.findViewByPosition(0);
View child1 = mLayoutManager.findViewByPosition(mConfig.mOrientation == VERTICAL ? 1
: mConfig.mSpanCount);
assertNotNull(logPrefix + " child position 0 should be laid out", child0);
assertNotNull(logPrefix + " child position 0 should be laid out", child1);
logPrefix += " child1 pos:" + mLayoutManager.getPosition(child1);
if (mConfig.mOrientation == VERTICAL || !mConfig.mReverseLayout) {
assertTrue(logPrefix + " second child should be to the left of first child",
helper.getDecoratedEnd(child0) > helper.getDecoratedEnd(child1));
assertEquals(logPrefix + " first child should be right aligned",
helper.getDecoratedEnd(child0), helper.getEndAfterPadding());
} else {
assertTrue(logPrefix + " first child should be to the left of second child",
helper.getDecoratedStart(child1) >= helper.getDecoratedStart(child0));
assertEquals(logPrefix + " first child should be left aligned",
helper.getDecoratedStart(child0), helper.getStartAfterPadding());
}
checkForMainThreadException();
}
@Test
public void scrollBackAndPreservePositions() throws Throwable {
scrollBackAndPreservePositionsTest(false);
}
@Test
public void scrollBackAndPreservePositionsWithRestore() throws Throwable {
scrollBackAndPreservePositionsTest(true);
}
public void scrollBackAndPreservePositionsTest(final boolean saveRestoreInBetween)
throws Throwable {
setupByConfig(mConfig);
mAdapter.mOnBindCallback = new OnBindCallback() {
@Override
public void onBoundItem(TestViewHolder vh, int position) {
StaggeredGridLayoutManager.LayoutParams
lp = (StaggeredGridLayoutManager.LayoutParams) vh.itemView
.getLayoutParams();
lp.setFullSpan((position * 7) % (mConfig.mSpanCount + 1) == 0);
}
};
waitFirstLayout();
final int[] globalPositions = new int[mAdapter.getItemCount()];
Arrays.fill(globalPositions, Integer.MIN_VALUE);
final int scrollStep = (mLayoutManager.mPrimaryOrientation.getTotalSpace() / 10)
* (mConfig.mReverseLayout ? -1 : 1);
final int[] globalPos = new int[1];
runTestOnUiThread(new Runnable() {
@Override
public void run() {
int globalScrollPosition = 0;
while (globalPositions[mAdapter.getItemCount() - 1] == Integer.MIN_VALUE) {
for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
View child = mRecyclerView.getChildAt(i);
final int pos = mRecyclerView.getChildLayoutPosition(child);
if (globalPositions[pos] != Integer.MIN_VALUE) {
continue;
}
if (mConfig.mReverseLayout) {
globalPositions[pos] = globalScrollPosition +
mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
} else {
globalPositions[pos] = globalScrollPosition +
mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
}
}
globalScrollPosition += mLayoutManager.scrollBy(scrollStep,
mRecyclerView.mRecycler, mRecyclerView.mState);
}
if (DEBUG) {
Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
}
globalPos[0] = globalScrollPosition;
}
});
checkForMainThreadException();
if (saveRestoreInBetween) {
saveRestore(mConfig);
}
checkForMainThreadException();
runTestOnUiThread(new Runnable() {
@Override
public void run() {
int globalScrollPosition = globalPos[0];
// now scroll back and make sure global positions match
BitSet shouldTest = new BitSet(mAdapter.getItemCount());
shouldTest.set(0, mAdapter.getItemCount() - 1, true);
String assertPrefix = mConfig + ", restored in between:" + saveRestoreInBetween
+ " global pos must match when scrolling in reverse for position ";
int scrollAmount = Integer.MAX_VALUE;
while (!shouldTest.isEmpty() && scrollAmount != 0) {
for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
View child = mRecyclerView.getChildAt(i);
int pos = mRecyclerView.getChildLayoutPosition(child);
if (!shouldTest.get(pos)) {
continue;
}
shouldTest.clear(pos);
int globalPos;
if (mConfig.mReverseLayout) {
globalPos = globalScrollPosition +
mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
} else {
globalPos = globalScrollPosition +
mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
}
assertEquals(assertPrefix + pos,
globalPositions[pos], globalPos);
}
scrollAmount = mLayoutManager.scrollBy(-scrollStep,
mRecyclerView.mRecycler, mRecyclerView.mState);
globalScrollPosition += scrollAmount;
}
assertTrue("all views should be seen", shouldTest.isEmpty());
}
});
checkForMainThreadException();
}
private void saveRestore(final Config config) throws Throwable {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
try {
Parcelable savedState = mRecyclerView.onSaveInstanceState();
// we append a suffix to the parcelable to test out of bounds
String parcelSuffix = UUID.randomUUID().toString();
Parcel parcel = Parcel.obtain();
savedState.writeToParcel(parcel, 0);
parcel.writeString(parcelSuffix);
removeRecyclerView();
// reset for reading
parcel.setDataPosition(0);
// re-create
savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
RecyclerView restored = new RecyclerView(getActivity());
mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
config.mOrientation);
mLayoutManager.setGapStrategy(config.mGapStrategy);
restored.setLayoutManager(mLayoutManager);
// use the same adapter for Rect matching
restored.setAdapter(mAdapter);
restored.onRestoreInstanceState(savedState);
if (Looper.myLooper() == Looper.getMainLooper()) {
mLayoutManager.expectLayouts(1);
setRecyclerView(restored);
} else {
mLayoutManager.expectLayouts(1);
setRecyclerView(restored);
mLayoutManager.waitForLayout(2);
}
} catch (Throwable t) {
postExceptionToInstrumentation(t);
}
}
});
checkForMainThreadException();
}
@Test
public void getFirstLastChildrenTest() throws Throwable {
getFirstLastChildrenTest(false);
}
@Test
public void getFirstLastChildrenTestProvideArray() throws Throwable {
getFirstLastChildrenTest(true);
}
public void getFirstLastChildrenTest(final boolean provideArr) throws Throwable {
setupByConfig(mConfig);
waitFirstLayout();
Runnable viewInBoundsTest = new Runnable() {
@Override
public void run() {
VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
final String boundsLog = mLayoutManager.getBoundsLog();
VisibleChildren queryResult = new VisibleChildren(mLayoutManager.getSpanCount());
queryResult.findFirstPartialVisibleClosestToStart = mLayoutManager
.findFirstVisibleItemClosestToStart(false, true);
queryResult.findFirstPartialVisibleClosestToEnd = mLayoutManager
.findFirstVisibleItemClosestToEnd(false, true);
queryResult.firstFullyVisiblePositions = mLayoutManager
.findFirstCompletelyVisibleItemPositions(
provideArr ? new int[mLayoutManager.getSpanCount()] : null);
queryResult.firstVisiblePositions = mLayoutManager
.findFirstVisibleItemPositions(
provideArr ? new int[mLayoutManager.getSpanCount()] : null);
queryResult.lastFullyVisiblePositions = mLayoutManager
.findLastCompletelyVisibleItemPositions(
provideArr ? new int[mLayoutManager.getSpanCount()] : null);
queryResult.lastVisiblePositions = mLayoutManager
.findLastVisibleItemPositions(
provideArr ? new int[mLayoutManager.getSpanCount()] : null);
assertEquals(mConfig + ":\nfirst visible child should match traversal result\n"
+ "traversed:" + visibleChildren + "\n"
+ "queried:" + queryResult + "\n"
+ boundsLog, visibleChildren, queryResult
);
}
};
runTestOnUiThread(viewInBoundsTest);
// smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
// case
final int scrollPosition = mAdapter.getItemCount();
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mRecyclerView.smoothScrollToPosition(scrollPosition);
}
});
while (mLayoutManager.isSmoothScrolling() ||
mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
runTestOnUiThread(viewInBoundsTest);
checkForMainThreadException();
Thread.sleep(400);
}
// delete all items
mLayoutManager.expectLayouts(2);
mAdapter.deleteAndNotify(0, mAdapter.getItemCount());
mLayoutManager.waitForLayout(2);
// test empty case
runTestOnUiThread(viewInBoundsTest);
// set a new adapter with huge items to test full bounds check
mLayoutManager.expectLayouts(1);
final int totalSpace = mLayoutManager.mPrimaryOrientation.getTotalSpace();
final TestAdapter newAdapter = new TestAdapter(100) {
@Override
public void onBindViewHolder(TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
if (mConfig.mOrientation == LinearLayoutManager.HORIZONTAL) {
holder.itemView.setMinimumWidth(totalSpace + 100);
} else {
holder.itemView.setMinimumHeight(totalSpace + 100);
}
}
};
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mRecyclerView.setAdapter(newAdapter);
}
});
mLayoutManager.waitForLayout(2);
runTestOnUiThread(viewInBoundsTest);
checkForMainThreadException();
// smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
// case
runTestOnUiThread(new Runnable() {
@Override
public void run() {
final int diff;
if (mConfig.mReverseLayout) {
diff = -1;
} else {
diff = 1;
}
final int distance = diff * 10;
if (mConfig.mOrientation == HORIZONTAL) {
mRecyclerView.scrollBy(distance, 0);
} else {
mRecyclerView.scrollBy(0, distance);
}
}
});
runTestOnUiThread(viewInBoundsTest);
checkForMainThreadException();
}
@Test
public void viewSnapTest() throws Throwable {
final Config config = ((Config) mConfig.clone()).itemCount(mConfig.mSpanCount + 1);
setupByConfig(config);
mAdapter.mOnBindCallback = new OnBindCallback() {
@Override
void onBoundItem(TestViewHolder vh, int position) {
StaggeredGridLayoutManager.LayoutParams
lp = (StaggeredGridLayoutManager.LayoutParams) vh.itemView
.getLayoutParams();
if (config.mOrientation == HORIZONTAL) {
lp.width = mRecyclerView.getWidth() / 3;
} else {
lp.height = mRecyclerView.getHeight() / 3;
}
}
@Override
boolean assignRandomSize() {
return false;
}
};
waitFirstLayout();
// run these tests twice. once initial layout, once after scroll
String logSuffix = "";
for (int i = 0; i < 2; i++) {
Map<Item, Rect> itemRectMap = mLayoutManager.collectChildCoordinates();
Rect recyclerViewBounds = getDecoratedRecyclerViewBounds();
// workaround for SGLM's span distribution issue. Right now, it may leave gaps so we
// avoid it by setting its layout params directly
if (config.mOrientation == HORIZONTAL) {
recyclerViewBounds.bottom -= recyclerViewBounds.height() % config.mSpanCount;
} else {
recyclerViewBounds.right -= recyclerViewBounds.width() % config.mSpanCount;
}
Rect usedLayoutBounds = new Rect();
for (Rect rect : itemRectMap.values()) {
usedLayoutBounds.union(rect);
}
if (DEBUG) {
Log.d(TAG, "testing view snapping (" + logSuffix + ") for config " + config);
}
if (config.mOrientation == VERTICAL) {
assertEquals(config + " there should be no gap on left" + logSuffix,
usedLayoutBounds.left, recyclerViewBounds.left);
assertEquals(config + " there should be no gap on right" + logSuffix,
usedLayoutBounds.right, recyclerViewBounds.right);
if (config.mReverseLayout) {
assertEquals(config + " there should be no gap on bottom" + logSuffix,
usedLayoutBounds.bottom, recyclerViewBounds.bottom);
assertTrue(config + " there should be some gap on top" + logSuffix,
usedLayoutBounds.top > recyclerViewBounds.top);
} else {
assertEquals(config + " there should be no gap on top" + logSuffix,
usedLayoutBounds.top, recyclerViewBounds.top);
assertTrue(config + " there should be some gap at the bottom" + logSuffix,
usedLayoutBounds.bottom < recyclerViewBounds.bottom);
}
} else {
assertEquals(config + " there should be no gap on top" + logSuffix,
usedLayoutBounds.top, recyclerViewBounds.top);
assertEquals(config + " there should be no gap at the bottom" + logSuffix,
usedLayoutBounds.bottom, recyclerViewBounds.bottom);
if (config.mReverseLayout) {
assertEquals(config + " there should be no on right" + logSuffix,
usedLayoutBounds.right, recyclerViewBounds.right);
assertTrue(config + " there should be some gap on left" + logSuffix,
usedLayoutBounds.left > recyclerViewBounds.left);
} else {
assertEquals(config + " there should be no gap on left" + logSuffix,
usedLayoutBounds.left, recyclerViewBounds.left);
assertTrue(config + " there should be some gap on right" + logSuffix,
usedLayoutBounds.right < recyclerViewBounds.right);
}
}
final int scroll = config.mReverseLayout ? -500 : 500;
scrollBy(scroll);
logSuffix = " scrolled " + scroll;
}
}
@Test
public void scrollToPositionWithOffsetTest() throws Throwable {
setupByConfig(mConfig);
waitFirstLayout();
OrientationHelper orientationHelper = OrientationHelper
.createOrientationHelper(mLayoutManager, mConfig.mOrientation);
Rect layoutBounds = getDecoratedRecyclerViewBounds();
// try scrolling towards head, should not affect anything
Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
scrollToPositionWithOffset(0, 20);
assertRectSetsEqual(mConfig + " trying to over scroll with offset should be no-op",
before, mLayoutManager.collectChildCoordinates());
// try offsetting some visible children
int testCount = 10;
while (testCount-- > 0) {
// get middle child
final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2);
final int position = mRecyclerView.getChildLayoutPosition(child);
final int startOffset = mConfig.mReverseLayout ?
orientationHelper.getEndAfterPadding() - orientationHelper
.getDecoratedEnd(child)
: orientationHelper.getDecoratedStart(child) - orientationHelper
.getStartAfterPadding();
final int scrollOffset = startOffset / 2;
mLayoutManager.expectLayouts(1);
scrollToPositionWithOffset(position, scrollOffset);
mLayoutManager.waitForLayout(2);
final int finalOffset = mConfig.mReverseLayout ?
orientationHelper.getEndAfterPadding() - orientationHelper
.getDecoratedEnd(child)
: orientationHelper.getDecoratedStart(child) - orientationHelper
.getStartAfterPadding();
assertEquals(mConfig + " scroll with offset on a visible child should work fine",
scrollOffset, finalOffset);
}
// try scrolling to invisible children
testCount = 10;
// we test above and below, one by one
int offsetMultiplier = -1;
while (testCount-- > 0) {
final TargetTuple target = findInvisibleTarget(mConfig);
mLayoutManager.expectLayouts(1);
final int offset = offsetMultiplier
* orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3;
scrollToPositionWithOffset(target.mPosition, offset);
mLayoutManager.waitForLayout(2);
final View child = mLayoutManager.findViewByPosition(target.mPosition);
assertNotNull(mConfig + " scrolling to a mPosition with offset " + offset
+ " should layout it", child);
final Rect bounds = mLayoutManager.getViewBounds(child);
if (DEBUG) {
Log.d(TAG, mConfig + " post scroll to invisible mPosition " + bounds + " in "
+ layoutBounds + " with offset " + offset);
}
if (mConfig.mReverseLayout) {
assertEquals(mConfig + " when scrolling with offset to an invisible in reverse "
+ "layout, its end should align with recycler view's end - offset",
orientationHelper.getEndAfterPadding() - offset,
orientationHelper.getDecoratedEnd(child)
);
} else {
assertEquals(mConfig + " when scrolling with offset to an invisible child in normal"
+ " layout its start should align with recycler view's start + "
+ "offset",
orientationHelper.getStartAfterPadding() + offset,
orientationHelper.getDecoratedStart(child)
);
}
offsetMultiplier *= -1;
}
}
@Test
public void scrollToPositionTest() throws Throwable {
setupByConfig(mConfig);
waitFirstLayout();
OrientationHelper orientationHelper = OrientationHelper
.createOrientationHelper(mLayoutManager, mConfig.mOrientation);
Rect layoutBounds = getDecoratedRecyclerViewBounds();
for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
View view = mLayoutManager.getChildAt(i);
Rect bounds = mLayoutManager.getViewBounds(view);
if (layoutBounds.contains(bounds)) {
Map<Item, Rect> initialBounds = mLayoutManager.collectChildCoordinates();
final int position = mRecyclerView.getChildLayoutPosition(view);
StaggeredGridLayoutManager.LayoutParams layoutParams
= (StaggeredGridLayoutManager.LayoutParams) (view.getLayoutParams());
TestViewHolder vh = (TestViewHolder) layoutParams.mViewHolder;
assertEquals("recycler view mPosition should match adapter mPosition", position,
vh.mBoundItem.mAdapterIndex);
if (DEBUG) {
Log.d(TAG, "testing scroll to visible mPosition at " + position
+ " " + bounds + " inside " + layoutBounds);
}
mLayoutManager.expectLayouts(1);
scrollToPosition(position);
mLayoutManager.waitForLayout(2);
if (DEBUG) {
view = mLayoutManager.findViewByPosition(position);
Rect newBounds = mLayoutManager.getViewBounds(view);
Log.d(TAG, "after scrolling to visible mPosition " +
bounds + " equals " + newBounds);
}
assertRectSetsEqual(
mConfig + "scroll to mPosition on fully visible child should be no-op",
initialBounds, mLayoutManager.collectChildCoordinates());
} else {
final int position = mRecyclerView.getChildLayoutPosition(view);
if (DEBUG) {
Log.d(TAG,
"child(" + position + ") not fully visible " + bounds + " not inside "
+ layoutBounds
+ mRecyclerView.getChildLayoutPosition(view)
);
}
mLayoutManager.expectLayouts(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mLayoutManager.scrollToPosition(position);
}
});
mLayoutManager.waitForLayout(2);
view = mLayoutManager.findViewByPosition(position);
bounds = mLayoutManager.getViewBounds(view);
if (DEBUG) {
Log.d(TAG, "after scroll to partially visible child " + bounds + " in "
+ layoutBounds);
}
assertTrue(mConfig
+ " after scrolling to a partially visible child, it should become fully "
+ " visible. " + bounds + " not inside " + layoutBounds,
layoutBounds.contains(bounds)
);
assertTrue(
mConfig + " when scrolling to a partially visible item, one of its edges "
+ "should be on the boundaries",
orientationHelper.getStartAfterPadding() ==
orientationHelper.getDecoratedStart(view)
|| orientationHelper.getEndAfterPadding() ==
orientationHelper.getDecoratedEnd(view));
}
}
// try scrolling to invisible children
int testCount = 10;
while (testCount-- > 0) {
final TargetTuple target = findInvisibleTarget(mConfig);
mLayoutManager.expectLayouts(1);
scrollToPosition(target.mPosition);
mLayoutManager.waitForLayout(2);
final View child = mLayoutManager.findViewByPosition(target.mPosition);
assertNotNull(mConfig + " scrolling to a mPosition should lay it out", child);
final Rect bounds = mLayoutManager.getViewBounds(child);
if (DEBUG) {
Log.d(TAG, mConfig + " post scroll to invisible mPosition " + bounds + " in "
+ layoutBounds);
}
assertTrue(mConfig + " scrolling to a mPosition should make it fully visible",
layoutBounds.contains(bounds));
if (target.mLayoutDirection == LAYOUT_START) {
assertEquals(
mConfig + " when scrolling to an invisible child above, its start should"
+ " align with recycler view's start",
orientationHelper.getStartAfterPadding(),
orientationHelper.getDecoratedStart(child)
);
} else {
assertEquals(mConfig + " when scrolling to an invisible child below, its end "
+ "should align with recycler view's end",
orientationHelper.getEndAfterPadding(),
orientationHelper.getDecoratedEnd(child)
);
}
}
}
@Test
public void scollByTest() throws Throwable {
setupByConfig(mConfig);
waitFirstLayout();
// try invalid scroll. should not happen
final View first = mLayoutManager.getChildAt(0);
OrientationHelper primaryOrientation = OrientationHelper
.createOrientationHelper(mLayoutManager, mConfig.mOrientation);
int scrollDist;
if (mConfig.mReverseLayout) {
scrollDist = primaryOrientation.getDecoratedMeasurement(first) / 2;
} else {
scrollDist = -primaryOrientation.getDecoratedMeasurement(first) / 2;
}
Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
scrollBy(scrollDist);
Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
assertRectSetsEqual(
mConfig + " if there are no more items, scroll should not happen (dt:" + scrollDist
+ ")",
before, after
);
scrollDist = -scrollDist * 3;
before = mLayoutManager.collectChildCoordinates();
scrollBy(scrollDist);
after = mLayoutManager.collectChildCoordinates();
int layoutStart = primaryOrientation.getStartAfterPadding();
int layoutEnd = primaryOrientation.getEndAfterPadding();
for (Map.Entry<Item, Rect> entry : before.entrySet()) {
Rect afterRect = after.get(entry.getKey());
// offset rect
if (mConfig.mOrientation == VERTICAL) {
entry.getValue().offset(0, -scrollDist);
} else {
entry.getValue().offset(-scrollDist, 0);
}
if (afterRect == null || afterRect.isEmpty()) {
// assert item is out of bounds
int start, end;
if (mConfig.mOrientation == VERTICAL) {
start = entry.getValue().top;
end = entry.getValue().bottom;
} else {
start = entry.getValue().left;
end = entry.getValue().right;
}
assertTrue(
mConfig + " if item is missing after relayout, it should be out of bounds."
+ "item start: " + start + ", end:" + end + " layout start:"
+ layoutStart +
", layout end:" + layoutEnd,
start <= layoutStart && end <= layoutEnd ||
start >= layoutEnd && end >= layoutEnd
);
} else {
assertEquals(mConfig + " Item should be laid out at the scroll offset coordinates",
entry.getValue(),
afterRect);
}
}
assertViewPositions(mConfig);
}
@Test
public void layoutOrderTest() throws Throwable {
setupByConfig(mConfig);
assertViewPositions(mConfig);
}
@Test
public void consistentRelayout() throws Throwable {
consistentRelayoutTest(mConfig, false);
}
@Test
public void consistentRelayoutWithFullSpanFirstChild() throws Throwable {
consistentRelayoutTest(mConfig, true);
}
@Test
public void dontRecycleViewsTranslatedOutOfBoundsFromStart() throws Throwable {
final Config config = ((Config) mConfig.clone()).itemCount(1000);
setupByConfig(config);
waitFirstLayout();
// pick position from child count so that it is not too far away
int pos = mRecyclerView.getChildCount() * 2;
smoothScrollToPosition(pos, true);
final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(pos);
OrientationHelper helper = mLayoutManager.mPrimaryOrientation;
int gap = helper.getDecoratedStart(vh.itemView);
scrollBy(gap);
gap = helper.getDecoratedStart(vh.itemView);
assertThat("test sanity", gap, is(0));
final int size = helper.getDecoratedMeasurement(vh.itemView);
AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
if (mConfig.mOrientation == HORIZONTAL) {
ViewCompat.setTranslationX(vh.itemView, size * 2);
} else {
ViewCompat.setTranslationY(vh.itemView, size * 2);
}
}
});
scrollBy(size * 2);
assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView))));
assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView));
assertThat(vh.getAdapterPosition(), is(pos));
scrollBy(size * 2);
assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView)));
}
@Test
public void dontRecycleViewsTranslatedOutOfBoundsFromEnd() throws Throwable {
final Config config = ((Config) mConfig.clone()).itemCount(1000);
setupByConfig(config);
waitFirstLayout();
// pick position from child count so that it is not too far away
int pos = mRecyclerView.getChildCount() * 2;
mLayoutManager.expectLayouts(1);
scrollToPosition(pos);
mLayoutManager.waitForLayout(2);
final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(pos);
OrientationHelper helper = mLayoutManager.mPrimaryOrientation;
int gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView);
scrollBy(-gap);
gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView);
assertThat("test sanity", gap, is(0));
final int size = helper.getDecoratedMeasurement(vh.itemView);
AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
if (mConfig.mOrientation == HORIZONTAL) {
ViewCompat.setTranslationX(vh.itemView, -size * 2);
} else {
ViewCompat.setTranslationY(vh.itemView, -size * 2);
}
}
});
scrollBy(-size * 2);
assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView))));
assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView));
assertThat(vh.getAdapterPosition(), is(pos));
scrollBy(-size * 2);
assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView)));
}
public void consistentRelayoutTest(Config config, boolean firstChildMultiSpan)
throws Throwable {
setupByConfig(config);
if (firstChildMultiSpan) {
mAdapter.mFullSpanItems.add(0);
}
waitFirstLayout();
// record all child positions
Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
requestLayoutOnUIThread(mRecyclerView);
Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
assertRectSetsEqual(
config + " simple re-layout, firstChildMultiSpan:" + firstChildMultiSpan, before,
after);
// scroll some to create inconsistency
View firstChild = mLayoutManager.getChildAt(0);
final int firstChildStartBeforeScroll = mLayoutManager.mPrimaryOrientation
.getDecoratedStart(firstChild);
int distance = mLayoutManager.mPrimaryOrientation.getDecoratedMeasurement(firstChild) / 2;
if (config.mReverseLayout) {
distance *= -1;
}
scrollBy(distance);
waitForMainThread(2);
assertTrue("scroll by should move children", firstChildStartBeforeScroll !=
mLayoutManager.mPrimaryOrientation.getDecoratedStart(firstChild));
before = mLayoutManager.collectChildCoordinates();
mLayoutManager.expectLayouts(1);
requestLayoutOnUIThread(mRecyclerView);
mLayoutManager.waitForLayout(2);
after = mLayoutManager.collectChildCoordinates();
assertRectSetsEqual(config + " simple re-layout after scroll", before, after);
}
}