blob: 3edcce5cb23c6db564d5972cdb3ec15995297468 [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 static android.support.v7.widget.LinearLayoutManager.VERTICAL;
import static android.support.v7.widget.StaggeredGridLayoutManager
.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE;
import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL;
import static android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v4.view.accessibility.AccessibilityRecordCompat;
import android.test.suitebuilder.annotation.MediumTest;
import android.text.TextUtils;
import android.util.Log;
import android.util.StateSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.EditText;
import android.widget.FrameLayout;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@MediumTest
public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManagerTest {
@Test
public void forceLayoutOnDetach() throws Throwable {
setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
waitFirstLayout();
assertFalse("test sanity", mRecyclerView.isLayoutRequested());
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mLayoutManager.onDetachedFromWindow(mRecyclerView, mRecyclerView.mRecycler);
}
});
assertTrue(mRecyclerView.isLayoutRequested());
}
@Test
public void areAllStartsTheSame() throws Throwable {
setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(300));
waitFirstLayout();
smoothScrollToPosition(100);
mLayoutManager.expectLayouts(1);
mAdapter.deleteAndNotify(0, 2);
mLayoutManager.waitForLayout(2);
smoothScrollToPosition(0);
assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual());
}
@Test
public void areAllEndsTheSame() throws Throwable {
setupByConfig(new Config(VERTICAL, true, 3, GAP_HANDLING_NONE).itemCount(300));
waitFirstLayout();
smoothScrollToPosition(100);
mLayoutManager.expectLayouts(1);
mAdapter.deleteAndNotify(0, 2);
mLayoutManager.waitForLayout(2);
smoothScrollToPosition(0);
assertFalse("all ends should not be the same", mLayoutManager.areAllEndsEqual());
}
@Test
public void getPositionsBeforeInitialization() throws Throwable {
setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
int[] positions = mLayoutManager.findFirstCompletelyVisibleItemPositions(null);
MatcherAssert.assertThat(positions,
CoreMatchers.is(new int[]{RecyclerView.NO_POSITION, RecyclerView.NO_POSITION,
RecyclerView.NO_POSITION}));
}
@Test
public void findLastInUnevenDistribution() throws Throwable {
setupByConfig(new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
.itemCount(5));
mAdapter.mOnBindCallback = new OnBindCallback() {
@Override
void onBoundItem(TestViewHolder vh, int position) {
LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
if (position == 1) {
lp.height = mRecyclerView.getHeight() - 10;
} else {
lp.height = 5;
}
vh.itemView.setMinimumHeight(0);
}
};
waitFirstLayout();
int[] into = new int[2];
mLayoutManager.findFirstCompletelyVisibleItemPositions(into);
assertEquals("first completely visible item from span 0 should be 0", 0, into[0]);
assertEquals("first completely visible item from span 1 should be 1", 1, into[1]);
mLayoutManager.findLastCompletelyVisibleItemPositions(into);
assertEquals("last completely visible item from span 0 should be 4", 4, into[0]);
assertEquals("last completely visible item from span 1 should be 1", 1, into[1]);
assertEquals("first fully visible child should be at position",
0, mRecyclerView.getChildViewHolder(mLayoutManager.
findFirstVisibleItemClosestToStart(true, true)).getPosition());
assertEquals("last fully visible child should be at position",
4, mRecyclerView.getChildViewHolder(mLayoutManager.
findFirstVisibleItemClosestToEnd(true, true)).getPosition());
assertEquals("first visible child should be at position",
0, mRecyclerView.getChildViewHolder(mLayoutManager.
findFirstVisibleItemClosestToStart(false, true)).getPosition());
assertEquals("last visible child should be at position",
4, mRecyclerView.getChildViewHolder(mLayoutManager.
findFirstVisibleItemClosestToEnd(false, true)).getPosition());
}
@Test
public void customWidthInHorizontal() throws Throwable {
customSizeInScrollDirectionTest(
new Config(HORIZONTAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
}
@Test
public void customHeightInVertical() throws Throwable {
customSizeInScrollDirectionTest(
new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
}
public void customSizeInScrollDirectionTest(final Config config) throws Throwable {
setupByConfig(config);
final Map<View, Integer> sizeMap = new HashMap<View, Integer>();
mAdapter.mOnBindCallback = new OnBindCallback() {
@Override
void onBoundItem(TestViewHolder vh, int position) {
final ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
final int size = 1 + position * 5;
if (config.mOrientation == HORIZONTAL) {
layoutParams.width = size;
} else {
layoutParams.height = size;
}
sizeMap.put(vh.itemView, size);
if (position == 3) {
getLp(vh.itemView).setFullSpan(true);
}
}
@Override
boolean assignRandomSize() {
return false;
}
};
waitFirstLayout();
assertTrue("[test sanity] some views should be laid out", sizeMap.size() > 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",
sizeMap.get(child).intValue(), size);
}
checkForMainThreadException();
}
@Test
public void gapHandlingWhenItemMovesToTop() throws Throwable {
gapHandlingWhenItemMovesToTopTest();
}
@Test
public void gapHandlingWhenItemMovesToTopWithFullSpan() throws Throwable {
gapHandlingWhenItemMovesToTopTest(0);
}
@Test
public void gapHandlingWhenItemMovesToTopWithFullSpan2() throws Throwable {
gapHandlingWhenItemMovesToTopTest(1);
}
public void gapHandlingWhenItemMovesToTopTest(int... fullSpanIndices) throws Throwable {
Config config = new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
config.itemCount(3);
setupByConfig(config);
mAdapter.mOnBindCallback = new OnBindCallback() {
@Override
void onBoundItem(TestViewHolder vh, int position) {
}
@Override
boolean assignRandomSize() {
return false;
}
};
for (int i : fullSpanIndices) {
mAdapter.mFullSpanItems.add(i);
}
waitFirstLayout();
mLayoutManager.expectLayouts(1);
mAdapter.moveItem(1, 0, true);
mLayoutManager.waitForLayout(2);
final Map<Item, Rect> desiredPositions = mLayoutManager.collectChildCoordinates();
// move back.
mLayoutManager.expectLayouts(1);
mAdapter.moveItem(0, 1, true);
mLayoutManager.waitForLayout(2);
mLayoutManager.expectLayouts(2);
mAdapter.moveAndNotify(1, 0);
mLayoutManager.waitForLayout(2);
Thread.sleep(1000);
getInstrumentation().waitForIdleSync();
checkForMainThreadException();
// item should be positioned properly
assertRectSetsEqual("final position after a move", desiredPositions,
mLayoutManager.collectChildCoordinates());
}
@Test
public void focusSearchFailureUp() throws Throwable {
focusSearchFailure(false);
}
@Test
public void focusSearchFailureDown() throws Throwable {
focusSearchFailure(true);
}
@Test
public void focusSearchFailureFromSubChild() throws Throwable {
setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
new GridTestAdapter(1000, VERTICAL) {
@Override
public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
FrameLayout fl = new FrameLayout(parent.getContext());
EditText editText = new EditText(parent.getContext());
fl.addView(editText);
editText.setEllipsize(TextUtils.TruncateAt.END);
return new TestViewHolder(fl);
}
@Override
public void onBindViewHolder(TestViewHolder holder, int position) {
Item item = mItems.get(position);
holder.mBoundItem = item;
((EditText) ((FrameLayout) holder.itemView).getChildAt(0)).setText(
item.mText + " (" + item.mId + ")");
}
});
waitFirstLayout();
ViewGroup lastChild = (ViewGroup) mRecyclerView.getChildAt(
mRecyclerView.getChildCount() - 1);
RecyclerView.ViewHolder lastViewHolder = mRecyclerView.getChildViewHolder(lastChild);
View subChildToFocus = lastChild.getChildAt(0);
requestFocus(subChildToFocus, true);
assertThat("test sanity", subChildToFocus.isFocused(), CoreMatchers.is(true));
focusSearch(subChildToFocus, View.FOCUS_FORWARD);
waitForIdleScroll(mRecyclerView);
checkForMainThreadException();
View focusedChild = mRecyclerView.getFocusedChild();
if (focusedChild == subChildToFocus.getParent()) {
focusSearch(focusedChild, View.FOCUS_FORWARD);
waitForIdleScroll(mRecyclerView);
focusedChild = mRecyclerView.getFocusedChild();
}
RecyclerView.ViewHolder containingViewHolder = mRecyclerView.findContainingViewHolder(
focusedChild);
assertTrue("new focused view should have a larger position "
+ lastViewHolder.getAdapterPosition() + " vs "
+ containingViewHolder.getAdapterPosition(),
lastViewHolder.getAdapterPosition() < containingViewHolder.getAdapterPosition());
}
public void focusSearchFailure(boolean scrollDown) throws Throwable {
int focusDir = scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP;
setupByConfig(new Config(VERTICAL, !scrollDown, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
, new GridTestAdapter(31, 1) {
RecyclerView mAttachedRv;
@Override
public TestViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
testViewHolder.itemView.setFocusable(true);
testViewHolder.itemView.setFocusableInTouchMode(true);
// Good to have colors for debugging
StateListDrawable stl = new StateListDrawable();
stl.addState(new int[]{android.R.attr.state_focused},
new ColorDrawable(Color.RED));
stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
testViewHolder.itemView.setBackground(stl);
return testViewHolder;
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
mAttachedRv = recyclerView;
}
@Override
public void onBindViewHolder(TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3);
}
});
/**
* 0 1 2
* 3 4 5
* 6 7 8
* 9 10 11
* 12 13 14
* 15 16 17
* 18 18 18
* 19
* 20 20 20
* 21 22
* 23 23 23
* 24 25 26
* 27 28 29
* 30
*/
mAdapter.mFullSpanItems.add(18);
mAdapter.mFullSpanItems.add(20);
mAdapter.mFullSpanItems.add(23);
waitFirstLayout();
View viewToFocus = mRecyclerView.findViewHolderForAdapterPosition(1).itemView;
assertTrue(requestFocus(viewToFocus, true));
assertSame(viewToFocus, mRecyclerView.getFocusedChild());
int pos = 1;
View focusedView = viewToFocus;
while (pos < 16) {
focusSearchAndWaitForScroll(focusedView, focusDir);
focusedView = mRecyclerView.getFocusedChild();
assertEquals(pos + 3,
mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
pos += 3;
}
for (int i : new int[]{18, 19, 20, 21, 23, 24}) {
focusSearchAndWaitForScroll(focusedView, focusDir);
focusedView = mRecyclerView.getFocusedChild();
assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
}
// now move right
focusSearch(focusedView, View.FOCUS_RIGHT);
waitForIdleScroll(mRecyclerView);
focusedView = mRecyclerView.getFocusedChild();
assertEquals(25, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
for (int i : new int[]{28, 30}) {
focusSearchAndWaitForScroll(focusedView, focusDir);
focusedView = mRecyclerView.getFocusedChild();
assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
}
}
private void focusSearchAndWaitForScroll(View focused, int dir) throws Throwable {
focusSearch(focused, dir);
waitForIdleScroll(mRecyclerView);
}
@Test
public void scrollToPositionWithPredictive() throws Throwable {
scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
removeRecyclerView();
scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
LinearLayoutManager.INVALID_OFFSET);
removeRecyclerView();
scrollToPositionWithPredictive(9, 20);
removeRecyclerView();
scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
}
public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)
throws Throwable {
setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL,
false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE));
waitFirstLayout();
mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
@Override
void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
RecyclerView rv = mLayoutManager.mRecyclerView;
if (state.isPreLayout()) {
assertEquals("pending scroll position should still be pending",
scrollPosition, mLayoutManager.mPendingScrollPosition);
if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
assertEquals("pending scroll position offset should still be pending",
scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
}
} else {
RecyclerView.ViewHolder vh = rv.findViewHolderForLayoutPosition(scrollPosition);
assertNotNull("scroll to position should work", vh);
if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
assertEquals("scroll offset should be applied properly",
mLayoutManager.getPaddingTop() + scrollOffset
+ ((RecyclerView.LayoutParams) vh.itemView
.getLayoutParams()).topMargin,
mLayoutManager.getDecoratedTop(vh.itemView));
}
}
}
};
mLayoutManager.expectLayouts(2);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
try {
mAdapter.addAndNotify(0, 1);
if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
mLayoutManager.scrollToPosition(scrollPosition);
} else {
mLayoutManager.scrollToPositionWithOffset(scrollPosition,
scrollOffset);
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
});
mLayoutManager.waitForLayout(2);
checkForMainThreadException();
}
@Test
public void moveGapHandling() throws Throwable {
Config config = new Config().spanCount(2).itemCount(40);
setupByConfig(config);
waitFirstLayout();
mLayoutManager.expectLayouts(2);
mAdapter.moveAndNotify(4, 1);
mLayoutManager.waitForLayout(2);
assertNull("moving item to upper should not cause gaps", mLayoutManager.hasGapsToFix());
}
@Test
public void updateAfterFullSpan() throws Throwable {
updateAfterFullSpanGapHandlingTest(0);
}
@Test
public void updateAfterFullSpan2() throws Throwable {
updateAfterFullSpanGapHandlingTest(20);
}
@Test
public void temporaryGapHandling() throws Throwable {
int fullSpanIndex = 200;
setupByConfig(new Config().spanCount(2).itemCount(500));
mAdapter.mFullSpanItems.add(fullSpanIndex);
waitFirstLayout();
smoothScrollToPosition(fullSpanIndex + 200);// go far away
assertNull("test sanity. full span item should not be visible",
mRecyclerView.findViewHolderForAdapterPosition(fullSpanIndex));
mLayoutManager.expectLayouts(1);
mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
mLayoutManager.waitForLayout(1);
smoothScrollToPosition(0);
mLayoutManager.expectLayouts(1);
smoothScrollToPosition(fullSpanIndex + 2 * (AVG_ITEM_PER_VIEW - 1));
String log = mLayoutManager.layoutToString("post gap");
mLayoutManager.assertNoLayout("if an interim gap is fixed, it should not cause a "
+ "relayout " + log, 2);
View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
assertNotNull("full span item should be there:\n" + log, fullSpan);
View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
assertNotNull("next view should be there\n" + log, view1);
View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
assertNotNull("+2 view should be there\n" + log, view2);
LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
assertEquals("view 1 span index", 0, lp1.getSpanIndex());
assertEquals("view 2 span index", 1, lp2.getSpanIndex());
assertEquals("no gap between span and view 1",
mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
assertEquals("no gap between span and view 2",
mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
}
public void updateAfterFullSpanGapHandlingTest(int fullSpanIndex) throws Throwable {
setupByConfig(new Config().spanCount(2).itemCount(100));
mAdapter.mFullSpanItems.add(fullSpanIndex);
waitFirstLayout();
smoothScrollToPosition(fullSpanIndex + 30);
mLayoutManager.expectLayouts(1);
mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
mLayoutManager.waitForLayout(1);
smoothScrollToPosition(fullSpanIndex);
// give it some time to fix the gap
Thread.sleep(500);
View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
assertEquals("view 1 span index", 0, lp1.getSpanIndex());
assertEquals("view 2 span index", 1, lp2.getSpanIndex());
assertEquals("no gap between span and view 1",
mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
assertEquals("no gap between span and view 2",
mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
}
@Test
public void innerGapHandling() throws Throwable {
innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
}
public void innerGapHandlingTest(int strategy) throws Throwable {
Config config = new Config().spanCount(3).itemCount(500);
setupByConfig(config);
mLayoutManager.setGapStrategy(strategy);
mAdapter.mFullSpanItems.add(100);
mAdapter.mFullSpanItems.add(104);
mAdapter.mViewsHaveEqualSize = true;
mAdapter.mOnBindCallback = new OnBindCallback() {
@Override
void onBoundItem(TestViewHolder vh, int position) {
}
@Override
void onCreatedViewHolder(TestViewHolder vh) {
super.onCreatedViewHolder(vh);
//make sure we have enough views
mAdapter.mSizeReference = mRecyclerView.getHeight() / 5;
}
};
waitFirstLayout();
mLayoutManager.expectLayouts(1);
scrollToPosition(400);
mLayoutManager.waitForLayout(2);
View view400 = mLayoutManager.findViewByPosition(400);
assertNotNull("test sanity, scrollToPos should succeed", view400);
assertTrue("test sanity, view should be visible top",
mLayoutManager.mPrimaryOrientation.getDecoratedStart(view400) >=
mLayoutManager.mPrimaryOrientation.getStartAfterPadding());
assertTrue("test sanity, view should be visible bottom",
mLayoutManager.mPrimaryOrientation.getDecoratedEnd(view400) <=
mLayoutManager.mPrimaryOrientation.getEndAfterPadding());
mLayoutManager.expectLayouts(2);
mAdapter.addAndNotify(101, 1);
mLayoutManager.waitForLayout(2);
checkForMainThreadException();
if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
mLayoutManager.expectLayouts(1);
}
// state
// now smooth scroll to 99 to trigger a layout around 100
mLayoutManager.validateChildren();
smoothScrollToPosition(99);
switch (strategy) {
case GAP_HANDLING_NONE:
assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0},
new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2},
new int[]{105, 0});
break;
case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
mLayoutManager.waitForLayout(2);
assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0},
new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0});
break;
}
}
@Test
public void fullSizeSpans() throws Throwable {
Config config = new Config().spanCount(5).itemCount(30);
setupByConfig(config);
mAdapter.mFullSpanItems.add(3);
waitFirstLayout();
assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2},
new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2},
new int[]{7, 3}, new int[]{8, 4});
}
void assertSpans(String msg, int[]... childSpanTuples) {
msg = msg + mLayoutManager.layoutToString("\n\n");
for (int i = 0; i < childSpanTuples.length; i++) {
assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]);
}
}
void assertSpan(String msg, int childPosition, int expectedSpan) {
View view = mLayoutManager.findViewByPosition(childPosition);
assertNotNull(msg + " view at position " + childPosition + " should exists", view);
assertEquals(msg + "[child:" + childPosition + "]", expectedSpan,
getLp(view).mSpan.mIndex);
}
@Test
public void partialSpanInvalidation() throws Throwable {
Config config = new Config().spanCount(5).itemCount(100);
setupByConfig(config);
for (int i = 20; i < mAdapter.getItemCount(); i += 20) {
mAdapter.mFullSpanItems.add(i);
}
waitFirstLayout();
smoothScrollToPosition(50);
int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30];
mAdapter.changeAndNotify(15, 2);
Thread.sleep(200);
assertEquals("Invalidation should happen within full span item boundaries", prevSpanId,
mLayoutManager.mLazySpanLookup.mData[30]);
assertEquals("item in invalidated range should have clear span id",
LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
smoothScrollToPosition(85);
int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85);
mAdapter.deleteAndNotify(55, 2);
Thread.sleep(200);
assertEquals("item in invalidated range should have clear span id",
LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83);
assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans,
newSpans, 0, 0, newSpans.length);
}
// Same as Arrays.copyOfRange but for API 7
private int[] copyOfRange(int[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0) {
throw new IllegalArgumentException(from + " > " + to);
}
int[] copy = new int[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
@Test
public void spanReassignmentsOnItemChange() throws Throwable {
Config config = new Config().spanCount(5);
setupByConfig(config);
waitFirstLayout();
smoothScrollToPosition(mAdapter.getItemCount() / 2);
final int changePosition = mAdapter.getItemCount() / 4;
mLayoutManager.expectLayouts(1);
mAdapter.changeAndNotify(changePosition, 1);
mLayoutManager.assertNoLayout("no layout should happen when an invisible child is updated",
1);
// delete an item before visible area
int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2;
assertTrue("test sanity", deletedPosition >= 0);
Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
if (DEBUG) {
Log.d(TAG, "before:");
for (Map.Entry<Item, Rect> entry : before.entrySet()) {
Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
}
}
mLayoutManager.expectLayouts(1);
mAdapter.deleteAndNotify(deletedPosition, 1);
mLayoutManager.waitForLayout(2);
assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it "
+ "should not affect the layout if it is not visible", before,
mLayoutManager.collectChildCoordinates()
);
deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2));
mLayoutManager.expectLayouts(1);
mAdapter.deleteAndNotify(deletedPosition, 1);
mLayoutManager.waitForLayout(2);
assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the "
+ "layout", before, mLayoutManager.collectChildCoordinates());
}
void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2,
int length) {
for (int i = 0; i < length; i++) {
assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i],
set2[start2 + i]);
}
}
@Test
public void spanCountChangeOnRestoreSavedState() throws Throwable {
Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE);
setupByConfig(config);
waitFirstLayout();
int beforeChildCount = mLayoutManager.getChildCount();
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);
removeRecyclerView();
RecyclerView restored = new RecyclerView(getActivity());
mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
mLayoutManager.setReverseLayout(config.mReverseLayout);
mLayoutManager.setGapStrategy(config.mGapStrategy);
restored.setLayoutManager(mLayoutManager);
// use the same adapter for Rect matching
restored.setAdapter(mAdapter);
restored.onRestoreInstanceState(savedState);
mLayoutManager.setSpanCount(1);
mLayoutManager.expectLayouts(1);
setRecyclerView(restored);
mLayoutManager.waitForLayout(2);
assertEquals("on saved state, reverse layout should be preserved",
config.mReverseLayout, mLayoutManager.getReverseLayout());
assertEquals("on saved state, orientation should be preserved",
config.mOrientation, mLayoutManager.getOrientation());
assertEquals("after setting new span count, layout manager should keep new value",
1, mLayoutManager.getSpanCount());
assertEquals("on saved state, gap strategy should be preserved",
config.mGapStrategy, mLayoutManager.getGapStrategy());
assertTrue("when span count is dramatically changed after restore, # of child views "
+ "should change", beforeChildCount > mLayoutManager.getChildCount());
// make sure LLM can layout all children. is some span info is leaked, this would crash
smoothScrollToPosition(mAdapter.getItemCount() - 1);
}
@Test
public void scrollAndClear() throws Throwable {
setupByConfig(new Config());
waitFirstLayout();
assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0);
mLayoutManager.expectLayouts(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mLayoutManager.scrollToPositionWithOffset(1, 0);
mAdapter.clearOnUIThread();
}
});
mLayoutManager.waitForLayout(2);
assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
}
@Test
public void accessibilityPositions() throws Throwable {
setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE));
waitFirstLayout();
final AccessibilityDelegateCompat delegateCompat = mRecyclerView
.getCompatAccessibilityDelegate();
final AccessibilityEvent event = AccessibilityEvent.obtain();
runTestOnUiThread(new Runnable() {
@Override
public void run() {
delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
}
});
final AccessibilityRecordCompat record = AccessibilityEventCompat
.asRecord(event);
final int start = mRecyclerView
.getChildLayoutPosition(
mLayoutManager.findFirstVisibleItemClosestToStart(false, true));
final int end = mRecyclerView
.getChildLayoutPosition(
mLayoutManager.findFirstVisibleItemClosestToEnd(false, true));
assertEquals("first item position should match",
Math.min(start, end), record.getFromIndex());
assertEquals("last item position should match",
Math.max(start, end), record.getToIndex());
}
}