/*
* 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.content.Context;
import android.graphics.Rect;
import android.os.Debug;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
import static java.util.concurrent.TimeUnit.SECONDS;
public class GridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
static final String TAG = "GridLayoutManagerTest";
static final boolean DEBUG = false;
WrappedGridLayoutManager mGlm;
GridTestAdapter mAdapter;
final List<Config> mBaseVariations = new ArrayList<Config>();
@Override
protected void setUp() throws Exception {
super.setUp();
for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
for (boolean reverseLayout : new boolean[]{false, true}) {
for (int spanCount : new int[]{1, 3, 4}) {
mBaseVariations.add(new Config(spanCount, orientation, reverseLayout));
}
}
}
}
public RecyclerView setupBasic(Config config) throws Throwable {
return setupBasic(config, new GridTestAdapter(config.mItemCount));
}
public RecyclerView setupBasic(Config config, GridTestAdapter testAdapter) throws Throwable {
RecyclerView recyclerView = new RecyclerView(getActivity());
mAdapter = testAdapter;
mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation,
config.mReverseLayout);
mAdapter.assignSpanSizeLookup(mGlm);
recyclerView.setAdapter(mAdapter);
recyclerView.setLayoutManager(mGlm);
return recyclerView;
}
public void waitForFirstLayout(RecyclerView recyclerView) throws Throwable {
mGlm.expectLayout(1);
setRecyclerView(recyclerView);
mGlm.waitForLayout(2);
}
public void testPredictiveSpanLookup1() throws Throwable {
predictiveSpanLookupTest(0, false);
}
public void testPredictiveSpanLookup2() throws Throwable {
predictiveSpanLookupTest(0, true);
}
public void testPredictiveSpanLookup3() throws Throwable {
predictiveSpanLookupTest(1, false);
}
public void testPredictiveSpanLookup4() throws Throwable {
predictiveSpanLookupTest(1, true);
}
public void predictiveSpanLookupTest(int remaining, boolean removeFromStart) throws Throwable {
RecyclerView recyclerView = setupBasic(new Config(3, 10));
mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position < 0 || position >= mAdapter.getItemCount()) {
postExceptionToInstrumentation(new AssertionError("position is not within " +
"adapter range. pos:" + position + ", adapter size:" +
mAdapter.getItemCount()));
}
return 1;
}
@Override
public int getSpanIndex(int position, int spanCount) {
if (position < 0 || position >= mAdapter.getItemCount()) {
postExceptionToInstrumentation(new AssertionError("position is not within " +
"adapter range. pos:" + position + ", adapter size:" +
mAdapter.getItemCount()));
}
return super.getSpanIndex(position, spanCount);
}
});
waitForFirstLayout(recyclerView);
checkForMainThreadException();
assertTrue("test sanity", mGlm.supportsPredictiveItemAnimations());
mGlm.expectLayout(2);
int deleteCnt = 10 - remaining;
int deleteStart = removeFromStart ? 0 : remaining;
mAdapter.deleteAndNotify(deleteStart, deleteCnt);
mGlm.waitForLayout(2);
checkForMainThreadException();
}
public void testCustomWidthInHorizontal() throws Throwable {
customSizeInScrollDirectionTest(new Config(3, HORIZONTAL, false));
}
public void testCustomHeightInVertical() throws Throwable {
customSizeInScrollDirectionTest(new Config(3, VERTICAL, false));
}
public void customSizeInScrollDirectionTest(final Config config) throws Throwable {
Boolean[] options = new Boolean[]{true, false};
for (boolean addMargins : options) {
for (boolean addDecorOffsets : options) {
customSizeInScrollDirectionTest(config, addDecorOffsets, addMargins);
}
}
}
public void customSizeInScrollDirectionTest(final Config config, boolean addDecorOffsets,
boolean addMarigns) throws Throwable {
final int decorOffset = addDecorOffsets ? 7 : 0;
final int margin = addMarigns ? 11 : 0;
final int[] sizePerPosition = new int[]{3, 5, 9, 21, 3, 5, 9, 6, 9, 1};
final int[] expectedSizePerPosition = new int[]{9, 9, 9, 21, 3, 5, 9, 9, 9, 1};
final GridTestAdapter testAdapter = new GridTestAdapter(10) {
@Override
public void onBindViewHolder(TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)
holder.itemView.getLayoutParams();
if (layoutParams == null) {
layoutParams = new ViewGroup.MarginLayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
holder.itemView.setLayoutParams(layoutParams);
}
final int size = sizePerPosition[position];
if (config.mOrientation == HORIZONTAL) {
layoutParams.width = size;
layoutParams.leftMargin = margin;
layoutParams.rightMargin = margin;
} else {
layoutParams.height = size;
layoutParams.topMargin = margin;
layoutParams.bottomMargin = margin;
}
}
};
testAdapter.setFullSpan(3, 5);
final RecyclerView rv = setupBasic(config, testAdapter);
if (addDecorOffsets) {
rv.addItemDecoration(new RecyclerView.ItemDecoration() {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (config.mOrientation == HORIZONTAL) {
outRect.set(decorOffset, 0, decorOffset, 0);
} else {
outRect.set(0, decorOffset, 0, decorOffset);
}
}
});
}
waitForFirstLayout(rv);
assertTrue("[test sanity] some views should be laid out", mRecyclerView.getChildCount() > 0);
for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
View child = mRecyclerView.getChildAt(i);
final int size = config.mOrientation == HORIZONTAL ? child.getWidth()
: child.getHeight();
assertEquals("child " + i + " should have the size specified in its layout params",
expectedSizePerPosition[i], size);
}
checkForMainThreadException();
}
public void testRTL() throws Throwable {
for (boolean changeRtlAfter : new boolean[]{false, true}) {
for (boolean oneLine : new boolean[]{false, true}) {
for (Config config : mBaseVariations) {
rtlTest(config, changeRtlAfter, oneLine);
removeRecyclerView();
}
}
}
}
void rtlTest(Config config, boolean changeRtlAfter, boolean oneLine) throws Throwable {
if (oneLine && config.mOrientation != VERTICAL) {
return;// nothing to test
}
if (config.mSpanCount == 1) {
config.mSpanCount = 2;
}
String logPrefix = config + ", changeRtlAfterLayout:" + changeRtlAfter + ", oneLine:" + oneLine;
config.mItemCount = 5;
if (oneLine) {
config.mSpanCount = config.mItemCount + 1;
} else {
config.mSpanCount = Math.min(config.mItemCount - 1, config.mSpanCount);
}
RecyclerView rv = setupBasic(config);
if (changeRtlAfter) {
waitForFirstLayout(rv);
mGlm.expectLayout(1);
mGlm.setFakeRtl(true);
mGlm.waitForLayout(2);
} else {
mGlm.mFakeRTL = true;
waitForFirstLayout(rv);
}
assertEquals("view should become rtl", true, mGlm.isLayoutRTL());
OrientationHelper helper = OrientationHelper.createHorizontalHelper(mGlm);
View child0 = mGlm.findViewByPosition(0);
final int secondChildPos = config.mOrientation == VERTICAL ? 1
: config.mSpanCount;
View child1 = mGlm.findViewByPosition(secondChildPos);
assertNotNull(logPrefix + " child position 0 should be laid out", child0);
assertNotNull(
logPrefix + " second child position " + (secondChildPos) + " should be laid out",
child1);
if (config.mOrientation == VERTICAL || !config.mReverseLayout) {
assertTrue(logPrefix + " second child should be to the left of first child",
helper.getDecoratedStart(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.getDecoratedEnd(child0));
assertEquals(logPrefix + " first child should be left aligned",
helper.getDecoratedStart(child0), helper.getStartAfterPadding());
}
checkForMainThreadException();
}
public void testMovingAGroupOffScreenForAddedItems() throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100));
final int[] maxId = new int[1];
maxId[0] = -1;
final SparseIntArray spanLookups = new SparseIntArray();
final AtomicBoolean enableSpanLookupLogging = new AtomicBoolean(false);
mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (maxId[0] > 0 && mAdapter.getItemAt(position).mId > maxId[0]) {
return 1;
} else if (enableSpanLookupLogging.get() && !rv.mState.isPreLayout()) {
spanLookups.put(position, spanLookups.get(position, 0) + 1);
}
return 3;
}
});
rv.getItemAnimator().setSupportsChangeAnimations(true);
waitForFirstLayout(rv);
View lastView = rv.getChildAt(rv.getChildCount() - 1);
final int lastPos = rv.getChildAdapterPosition(lastView);
maxId[0] = mAdapter.getItemAt(mAdapter.getItemCount() - 1).mId;
// now add a lot of items below this and those new views should have span size 3
enableSpanLookupLogging.set(true);
mGlm.expectLayout(2);
mAdapter.addAndNotify(lastPos - 2, 30);
mGlm.waitForLayout(2);
checkForMainThreadException();
assertEquals("last items span count should be queried twice", 2,
spanLookups.get(lastPos + 30));
}
public void testCachedBorders() throws Throwable {
List<Config> testConfigurations = new ArrayList<Config>(mBaseVariations);
testConfigurations.addAll(cachedBordersTestConfigs());
for (Config config : testConfigurations) {
gridCachedBorderstTest(config);
}
}
private void gridCachedBorderstTest(Config config) throws Throwable {
RecyclerView recyclerView = setupBasic(config);
waitForFirstLayout(recyclerView);
final boolean vertical = config.mOrientation == GridLayoutManager.VERTICAL;
final int expectedSizeSum = vertical ? recyclerView.getWidth() : recyclerView.getHeight();
final int lastVisible = mGlm.findLastVisibleItemPosition();
for (int i = 0; i < lastVisible; i += config.mSpanCount) {
if ((i+1)*config.mSpanCount - 1 < lastVisible) {
int childrenSizeSum = 0;
for (int j = 0; j < config.mSpanCount; j++) {
View child = recyclerView.getChildAt(i * config.mSpanCount + j);
childrenSizeSum += vertical ? child.getWidth() : child.getHeight();
}
assertEquals(expectedSizeSum, childrenSizeSum);
}
}
removeRecyclerView();
}
private List<Config> cachedBordersTestConfigs() {
ArrayList<Config> configs = new ArrayList<Config>();
final int [] spanCounts = new int[]{88, 279, 741};
final int [] spanPerItem = new int[]{11, 9, 13};
for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
for (boolean reverseLayout : new boolean[]{false, true}) {
for (int i = 0 ; i < spanCounts.length; i++) {
Config config = new Config(spanCounts[i], orientation, reverseLayout);
config.mSpanPerItem = spanPerItem[i];
configs.add(config);
}
}
}
return configs;
}
public void testLayoutParams() throws Throwable {
layoutParamsTest(GridLayoutManager.HORIZONTAL);
removeRecyclerView();
layoutParamsTest(GridLayoutManager.VERTICAL);
}
public void testHorizontalAccessibilitySpanIndices() throws Throwable {
accessibilitySpanIndicesTest(HORIZONTAL);
}
public void testVerticalAccessibilitySpanIndices() throws Throwable {
accessibilitySpanIndicesTest(VERTICAL);
}
public void accessibilitySpanIndicesTest(int orientation) throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, orientation, false));
waitForFirstLayout(recyclerView);
final AccessibilityDelegateCompat delegateCompat = mRecyclerView
.getCompatAccessibilityDelegate().getItemDelegate();
final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
final View chosen = recyclerView.getChildAt(recyclerView.getChildCount() - 2);
final int position = recyclerView.getChildLayoutPosition(chosen);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info);
}
});
GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info
.getCollectionItemInfo();
assertNotNull(itemInfo);
assertEquals("result should have span group position",
ssl.getSpanGroupIndex(position, mGlm.getSpanCount()),
orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex());
assertEquals("result should have span index",
ssl.getSpanIndex(position, mGlm.getSpanCount()),
orientation == HORIZONTAL ? itemInfo.getRowIndex() : itemInfo.getColumnIndex());
assertEquals("result should have span size",
ssl.getSpanSize(position),
orientation == HORIZONTAL ? itemInfo.getRowSpan() : itemInfo.getColumnSpan());
}
public GridLayoutManager.LayoutParams ensureGridLp(View view) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
GridLayoutManager.LayoutParams glp;
if (lp instanceof GridLayoutManager.LayoutParams) {
glp = (GridLayoutManager.LayoutParams) lp;
} else if (lp == null) {
glp = (GridLayoutManager.LayoutParams) mGlm
.generateDefaultLayoutParams();
view.setLayoutParams(glp);
} else {
glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp);
view.setLayoutParams(glp);
}
return glp;
}
public void layoutParamsTest(final int orientation) throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation),
new GridTestAdapter(100) {
@Override
public void onBindViewHolder(TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
int val = 0;
switch (position % 5) {
case 0:
val = 10;
break;
case 1:
val = 30;
break;
case 2:
val = GridLayoutManager.LayoutParams.WRAP_CONTENT;
break;
case 3:
val = GridLayoutManager.LayoutParams.FILL_PARENT;
break;
case 4:
val = 200;
break;
}
if (orientation == GridLayoutManager.VERTICAL) {
glp.height = val;
} else {
glp.width = val;
}
holder.itemView.setLayoutParams(glp);
}
});
waitForFirstLayout(rv);
final OrientationHelper helper = mGlm.mOrientationHelper;
final int firstRowSize = Math.max(30, getSize(mGlm.findViewByPosition(2)));
assertEquals(firstRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(0)));
assertEquals(firstRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(1)));
assertEquals(firstRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(2)));
assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(0)));
assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(1)));
assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(2)));
final int secondRowSize = Math.max(200, getSize(mGlm.findViewByPosition(3)));
assertEquals(secondRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(3)));
assertEquals(secondRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(4)));
assertEquals(secondRowSize,
helper.getDecoratedMeasurement(mGlm.findViewByPosition(5)));
assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(3)));
assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(4)));
assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5)));
}
private int getSize(View view) {
if (mGlm.getOrientation() == GridLayoutManager.HORIZONTAL) {
return view.getWidth();
}
return view.getHeight();
}
public void testAnchorUpdate() throws InterruptedException {
GridLayoutManager glm = new GridLayoutManager(getActivity(), 11);
final GridLayoutManager.SpanSizeLookup spanSizeLookup
= new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position > 200) {
return 100;
}
if (position > 20) {
return 2;
}
return 1;
}
};
glm.setSpanSizeLookup(spanSizeLookup);
glm.mAnchorInfo.mPosition = 11;
RecyclerView.State state = new RecyclerView.State();
mRecyclerView = new RecyclerView(getActivity());
state.mItemCount = 1000;
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo);
assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition);
glm.mAnchorInfo.mPosition = 13;
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo);
assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition);
glm.mAnchorInfo.mPosition = 23;
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo);
assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition);
glm.mAnchorInfo.mPosition = 35;
glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo);
assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition);
}
public void testSpanLookup() {
spanLookupTest(false);
}
public void testSpanLookupWithCache() {
spanLookupTest(true);
}
public void testSpanLookupCache() {
final GridLayoutManager.SpanSizeLookup ssl
= new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position > 6) {
return 2;
}
return 1;
}
};
ssl.setSpanIndexCacheEnabled(true);
assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(2));
ssl.getCachedSpanIndex(4, 5);
assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(3));
// this should not happen and if happens, it is better to return -1
assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(5));
assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(100));
ssl.getCachedSpanIndex(6, 5);
assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(6));
assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
ssl.getCachedSpanIndex(12, 5);
assertEquals("reference child before", 12, ssl.findReferenceIndexFromCache(13));
assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(12));
assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
for (int i = 0; i < 6; i++) {
ssl.getCachedSpanIndex(i, 5);
}
for (int i = 1; i < 7; i++) {
assertEquals("reference child right before " + i, i - 1,
ssl.findReferenceIndexFromCache(i));
}
assertEquals("reference child before 0 ", -1, ssl.findReferenceIndexFromCache(0));
}
public void spanLookupTest(boolean enableCache) {
final GridLayoutManager.SpanSizeLookup ssl
= new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position > 200) {
return 100;
}
if (position > 6) {
return 2;
}
return 1;
}
};
ssl.setSpanIndexCacheEnabled(enableCache);
assertEquals(0, ssl.getCachedSpanIndex(0, 5));
assertEquals(4, ssl.getCachedSpanIndex(4, 5));
assertEquals(0, ssl.getCachedSpanIndex(5, 5));
assertEquals(1, ssl.getCachedSpanIndex(6, 5));
assertEquals(2, ssl.getCachedSpanIndex(7, 5));
assertEquals(2, ssl.getCachedSpanIndex(9, 5));
assertEquals(0, ssl.getCachedSpanIndex(8, 5));
}
public void testRemoveAnchorItem() throws Throwable {
removeAnchorItemTest(
new Config(3, 0).orientation(VERTICAL).reverseLayout(false), 100, 0);
}
public void testRemoveAnchorItemReverse() throws Throwable {
removeAnchorItemTest(
new Config(3, 0).orientation(VERTICAL).reverseLayout(true), 100,
0);
}
public void testRemoveAnchorItemHorizontal() throws Throwable {
removeAnchorItemTest(
new Config(3, 0).orientation(HORIZONTAL).reverseLayout(
false), 100, 0);
}
public void testRemoveAnchorItemReverseHorizontal() throws Throwable {
removeAnchorItemTest(
new Config(3, 0).orientation(HORIZONTAL).reverseLayout(true),
100, 0);
}
/**
* This tests a regression where predictive animations were not working as expected when the
* first item is removed and there aren't any more items to add from that direction.
* First item refers to the default anchor item.
*/
public void removeAnchorItemTest(final Config config, int adapterSize,
final int removePos) throws Throwable {
GridTestAdapter adapter = new GridTestAdapter(adapterSize) {
@Override
public void onBindViewHolder(TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (!(lp instanceof ViewGroup.MarginLayoutParams)) {
lp = new ViewGroup.MarginLayoutParams(0, 0);
holder.itemView.setLayoutParams(lp);
}
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
final int maxSize;
if (config.mOrientation == HORIZONTAL) {
maxSize = mRecyclerView.getWidth();
mlp.height = ViewGroup.MarginLayoutParams.FILL_PARENT;
} else {
maxSize = mRecyclerView.getHeight();
mlp.width = ViewGroup.MarginLayoutParams.FILL_PARENT;
}
final int desiredSize;
if (position == removePos) {
// make it large
desiredSize = maxSize / 4;
} else {
// make it small
desiredSize = maxSize / 8;
}
if (config.mOrientation == HORIZONTAL) {
mlp.width = desiredSize;
} else {
mlp.height = desiredSize;
}
}
};
RecyclerView recyclerView = setupBasic(config, adapter);
waitForFirstLayout(recyclerView);
final int childCount = mGlm.getChildCount();
RecyclerView.ViewHolder toBeRemoved = null;
List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>();
for (int i = 0; i < childCount; i++) {
View child = mGlm.getChildAt(i);
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
if (holder.getAdapterPosition() == removePos) {
toBeRemoved = holder;
} else {
toBeMoved.add(holder);
}
}
assertNotNull("test sanity", toBeRemoved);
assertEquals("test sanity", childCount - 1, toBeMoved.size());
LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator();
mRecyclerView.setItemAnimator(loggingItemAnimator);
loggingItemAnimator.reset();
loggingItemAnimator.expectRunPendingAnimationsCall(1);
mGlm.expectLayout(2);
adapter.deleteAndNotify(removePos, 1);
mGlm.waitForLayout(1);
loggingItemAnimator.waitForPendingAnimationsCall(2);
assertTrue("removed child should receive remove animation",
loggingItemAnimator.mRemoveVHs.contains(toBeRemoved));
for (RecyclerView.ViewHolder vh : toBeMoved) {
assertTrue("view holder should be in moved list",
loggingItemAnimator.mMoveVHs.contains(vh));
}
List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>();
for (int i = 0; i < mGlm.getChildCount(); i++) {
View child = mGlm.getChildAt(i);
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
if (toBeRemoved != holder && !toBeMoved.contains(holder)) {
newHolders.add(holder);
}
}
assertTrue("some new children should show up for the new space", newHolders.size() > 0);
assertEquals("no items should receive animate add since they are not new", 0,
loggingItemAnimator.mAddVHs.size());
for (RecyclerView.ViewHolder holder : newHolders) {
assertTrue("new holder should receive a move animation",
loggingItemAnimator.mMoveVHs.contains(holder));
}
// for removed view, 3 for new row
assertTrue("control against adding too many children due to bad layout state preparation."
+ " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(),
mRecyclerView.getChildCount() <= childCount + 1 + 3);
}
public void testSpanGroupIndex() {
final GridLayoutManager.SpanSizeLookup ssl
= new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (position > 200) {
return 100;
}
if (position > 6) {
return 2;
}
return 1;
}
};
assertEquals(0, ssl.getSpanGroupIndex(0, 5));
assertEquals(0, ssl.getSpanGroupIndex(4, 5));
assertEquals(1, ssl.getSpanGroupIndex(5, 5));
assertEquals(1, ssl.getSpanGroupIndex(6, 5));
assertEquals(1, ssl.getSpanGroupIndex(7, 5));
assertEquals(2, ssl.getSpanGroupIndex(9, 5));
assertEquals(2, ssl.getSpanGroupIndex(8, 5));
}
public void testNotifyDataSetChange() throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 100));
final GridLayoutManager.SpanSizeLookup ssl = mGlm.getSpanSizeLookup();
ssl.setSpanIndexCacheEnabled(true);
waitForFirstLayout(recyclerView);
assertTrue("some positions should be cached", ssl.mSpanIndexCache.size() > 0);
final Callback callback = new Callback() {
@Override
public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (!state.isPreLayout()) {
assertEquals("cache should be empty", 0, ssl.mSpanIndexCache.size());
}
}
@Override
public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (!state.isPreLayout()) {
assertTrue("some items should be cached", ssl.mSpanIndexCache.size() > 0);
}
}
};
mGlm.mCallbacks.add(callback);
mGlm.expectLayout(2);
mAdapter.deleteAndNotify(2, 3);
mGlm.waitForLayout(2);
checkForMainThreadException();
}
public void testUnevenHeights() throws Throwable {
final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
new HashMap<Integer, RecyclerView.ViewHolder>();
RecyclerView recyclerView = setupBasic(new Config(3, 3), new GridTestAdapter(3) {
@Override
public void onBindViewHolder(TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
glp.height = 50 + position * 50;
viewHolderMap.put(position, holder);
}
});
waitForFirstLayout(recyclerView);
for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
assertEquals("all items should get max height", 150,
vh.itemView.getHeight());
}
for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
assertEquals("all items should have measured the max height", 150,
vh.itemView.getMeasuredHeight());
}
}
public void testUnevenWidths() throws Throwable {
final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
new HashMap<Integer, RecyclerView.ViewHolder>();
RecyclerView recyclerView = setupBasic(new Config(3, HORIZONTAL, false),
new GridTestAdapter(3) {
@Override
public void onBindViewHolder(TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
glp.width = 50 + position * 50;
viewHolderMap.put(position, holder);
}
});
waitForFirstLayout(recyclerView);
for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
assertEquals("all items should get max width", 150,
vh.itemView.getWidth());
}
for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
assertEquals("all items should have measured the max width", 150,
vh.itemView.getMeasuredWidth());
}
}
public void testScrollBackAndPreservePositions() throws Throwable {
for (Config config : mBaseVariations) {
config.mItemCount = 150;
scrollBackAndPreservePositionsTest(config);
removeRecyclerView();
}
}
public void testSpanSizeChange() throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100));
waitForFirstLayout(rv);
assertTrue(mGlm.supportsPredictiveItemAnimations());
mGlm.expectLayout(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mGlm.setSpanCount(5);
assertFalse(mGlm.supportsPredictiveItemAnimations());
}
});
checkForMainThreadException();
mGlm.waitForLayout(2);
mGlm.expectLayout(2);
mAdapter.deleteAndNotify(3, 2);
mGlm.waitForLayout(2);
assertTrue(mGlm.supportsPredictiveItemAnimations());
}
public void testCacheSpanIndices() throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100));
mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true);
waitForFirstLayout(rv);
GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
assertTrue("cache should be filled", mGlm.mSpanSizeLookup.mSpanIndexCache.size() > 0);
assertEquals("item index 5 should be in span 2", 2,
getLp(mGlm.findViewByPosition(5)).getSpanIndex());
mGlm.expectLayout(2);
mAdapter.mFullSpanItems.add(4);
mAdapter.changeAndNotify(4, 1);
mGlm.waitForLayout(2);
assertEquals("item index 5 should be in span 2", 0,
getLp(mGlm.findViewByPosition(5)).getSpanIndex());
}
GridLayoutManager.LayoutParams getLp(View view) {
return (GridLayoutManager.LayoutParams) view.getLayoutParams();
}
public void scrollBackAndPreservePositionsTest(final Config config) throws Throwable {
final RecyclerView rv = setupBasic(config);
for (int i = 1; i < mAdapter.getItemCount(); i += config.mSpanCount + 2) {
mAdapter.setFullSpan(i);
}
waitForFirstLayout(rv);
final int[] globalPositions = new int[mAdapter.getItemCount()];
Arrays.fill(globalPositions, Integer.MIN_VALUE);
final int scrollStep = (mGlm.mOrientationHelper.getTotalSpace() / 20)
* (config.mReverseLayout ? -1 : 1);
final String logPrefix = config.toString();
final int[] globalPos = new int[1];
runTestOnUiThread(new Runnable() {
@Override
public void run() {
assertSame("test sanity", mRecyclerView, rv);
int globalScrollPosition = 0;
int visited = 0;
while (visited < mAdapter.getItemCount()) {
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;
}
visited++;
GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
child.getLayoutParams();
if (config.mReverseLayout) {
globalPositions[pos] = globalScrollPosition +
mGlm.mOrientationHelper.getDecoratedEnd(child);
} else {
globalPositions[pos] = globalScrollPosition +
mGlm.mOrientationHelper.getDecoratedStart(child);
}
assertEquals(logPrefix + " span index should match",
mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()),
lp.getSpanIndex());
}
int scrolled = mGlm.scrollBy(scrollStep,
mRecyclerView.mRecycler, mRecyclerView.mState);
globalScrollPosition += scrolled;
if (scrolled == 0) {
assertEquals(
logPrefix + " If scroll is complete, all views should be visited",
visited, mAdapter.getItemCount());
}
}
if (DEBUG) {
Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
}
globalPos[0] = globalScrollPosition;
}
});
checkForMainThreadException();
// test sanity, ensure scroll happened
runTestOnUiThread(new Runnable() {
@Override
public void run() {
final int childCount = mGlm.getChildCount();
final BitSet expectedPositions = new BitSet();
for (int i = 0; i < childCount; i ++) {
expectedPositions.set(mAdapter.getItemCount() - i - 1);
}
for (int i = 0; i <childCount; i ++) {
final View view = mGlm.getChildAt(i);
int position = mGlm.getPosition(view);
assertTrue("child position should be in last page", expectedPositions.get(position));
}
}
});
getInstrumentation().waitForIdleSync();
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 = config
+ " 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;
}
GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
child.getLayoutParams();
shouldTest.clear(pos);
int globalPos;
if (config.mReverseLayout) {
globalPos = globalScrollPosition +
mGlm.mOrientationHelper.getDecoratedEnd(child);
} else {
globalPos = globalScrollPosition +
mGlm.mOrientationHelper.getDecoratedStart(child);
}
assertEquals(assertPrefix + pos,
globalPositions[pos], globalPos);
assertEquals("span index should match",
mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()),
lp.getSpanIndex());
}
scrollAmount = mGlm.scrollBy(-scrollStep,
mRecyclerView.mRecycler, mRecyclerView.mState);
globalScrollPosition += scrollAmount;
}
assertTrue("all views should be seen", shouldTest.isEmpty());
}
});
checkForMainThreadException();
}
class WrappedGridLayoutManager extends GridLayoutManager {
CountDownLatch mLayoutLatch;
List<Callback> mCallbacks = new ArrayList<Callback>();
Boolean mFakeRTL;
public WrappedGridLayoutManager(Context context, int spanCount) {
super(context, spanCount);
}
public WrappedGridLayoutManager(Context context, int spanCount, int orientation,
boolean reverseLayout) {
super(context, spanCount, orientation, reverseLayout);
}
@Override
protected boolean isLayoutRTL() {
return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL;
}
public void setFakeRtl(Boolean fakeRtl) {
mFakeRTL = fakeRtl;
try {
requestLayoutOnUIThread(mRecyclerView);
} catch (Throwable throwable) {
postExceptionToInstrumentation(throwable);
}
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
for (Callback callback : mCallbacks) {
callback.onBeforeLayout(recycler, state);
}
super.onLayoutChildren(recycler, state);
for (Callback callback : mCallbacks) {
callback.onAfterLayout(recycler, state);
}
} catch (Throwable t) {
postExceptionToInstrumentation(t);
}
mLayoutLatch.countDown();
}
@Override
LayoutState createLayoutState() {
return new LayoutState() {
@Override
View next(RecyclerView.Recycler recycler) {
final boolean hadMore = hasMore(mRecyclerView.mState);
final int position = mCurrentPosition;
View next = super.next(recycler);
assertEquals("if has more, should return a view", hadMore, next != null);
assertEquals("position of the returned view must match current position",
position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition());
return next;
}
};
}
public void expectLayout(int layoutCount) {
mLayoutLatch = new CountDownLatch(layoutCount);
}
public void waitForLayout(int seconds) throws InterruptedException {
mLayoutLatch.await(seconds, SECONDS);
}
}
class Config {
int mSpanCount;
int mOrientation = GridLayoutManager.VERTICAL;
int mItemCount = 1000;
int mSpanPerItem = 1;
boolean mReverseLayout = false;
Config(int spanCount, int itemCount) {
mSpanCount = spanCount;
mItemCount = itemCount;
}
public Config(int spanCount, int orientation, boolean reverseLayout) {
mSpanCount = spanCount;
mOrientation = orientation;
mReverseLayout = reverseLayout;
}
Config orientation(int orientation) {
mOrientation = orientation;
return this;
}
@Override
public String toString() {
return "Config{" +
"mSpanCount=" + mSpanCount +
", mOrientation=" + (mOrientation == GridLayoutManager.HORIZONTAL ? "h" : "v") +
", mItemCount=" + mItemCount +
", mReverseLayout=" + mReverseLayout +
'}';
}
public Config reverseLayout(boolean reverseLayout) {
mReverseLayout = reverseLayout;
return this;
}
}
class GridTestAdapter extends TestAdapter {
Set<Integer> mFullSpanItems = new HashSet<Integer>();
int mSpanPerItem = 1;
GridTestAdapter(int count) {
super(count);
}
GridTestAdapter(int count, int spanPerItem) {
super(count);
mSpanPerItem = spanPerItem;
}
void setFullSpan(int... items) {
for (int i : items) {
mFullSpanItems.add(i);
}
}
void assignSpanSizeLookup(final GridLayoutManager glm) {
glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return mFullSpanItems.contains(position) ? glm.getSpanCount() : mSpanPerItem;
}
});
}
}
class Callback {
public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
}
public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
}
}
}