blob: ae80cc9e0523bdc6ebf1322783f0808524738ea1 [file] [log] [blame]
/*
* Copyright 2018 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 androidx.recyclerview.widget;
import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
import static androidx.recyclerview.widget.StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
import static androidx.recyclerview.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE;
import static androidx.recyclerview.widget.StaggeredGridLayoutManager.HORIZONTAL;
import static androidx.recyclerview.widget.StaggeredGridLayoutManager.LayoutParams;
import static org.hamcrest.CoreMatchers.equalTo;
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.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 androidx.annotation.NonNull;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.test.filters.LargeTest;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@LargeTest
public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManagerTest {
@Test
public void layout_rvHasPaddingChildIsMatchParentVertical_childrenAreInsideParent()
throws Throwable {
layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(VERTICAL, false);
}
@Test
public void layout_rvHasPaddingChildIsMatchParentHorizontal_childrenAreInsideParent()
throws Throwable {
layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(HORIZONTAL, false);
}
@Test
public void layout_rvHasPaddingChildIsMatchParentVerticalFullSpan_childrenAreInsideParent()
throws Throwable {
layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(VERTICAL, true);
}
@Test
public void layout_rvHasPaddingChildIsMatchParentHorizontalFullSpan_childrenAreInsideParent()
throws Throwable {
layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(HORIZONTAL, true);
}
private void layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(
final int orientation, final boolean fullSpan)
throws Throwable {
setupByConfig(new Config(orientation, false, 1, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
new GridTestAdapter(10, orientation) {
@NonNull
@Override
public TestViewHolder onCreateViewHolder(
@NonNull ViewGroup parent, int viewType) {
View view = new View(parent.getContext());
StaggeredGridLayoutManager.LayoutParams layoutParams =
new StaggeredGridLayoutManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
layoutParams.setFullSpan(fullSpan);
view.setLayoutParams(layoutParams);
return new TestViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder, int position) {
// No actual binding needed, but we need to override this to prevent default
// behavior of GridTestAdapter.
}
});
mRecyclerView.setPadding(1, 2, 3, 4);
waitFirstLayout();
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
int childDimension;
int recyclerViewDimensionMinusPadding;
if (orientation == VERTICAL) {
childDimension = mRecyclerView.getChildAt(0).getHeight();
recyclerViewDimensionMinusPadding = mRecyclerView.getHeight()
- mRecyclerView.getPaddingTop()
- mRecyclerView.getPaddingBottom();
} else {
childDimension = mRecyclerView.getChildAt(0).getWidth();
recyclerViewDimensionMinusPadding = mRecyclerView.getWidth()
- mRecyclerView.getPaddingLeft()
- mRecyclerView.getPaddingRight();
}
assertThat(childDimension, equalTo(recyclerViewDimensionMinusPadding));
}
});
}
@Test
public void forceLayoutOnDetach() throws Throwable {
setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
waitFirstLayout();
assertFalse("test sanity", mRecyclerView.isLayoutRequested());
mActivityRule.runOnUiThread(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(2000);
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)).getPosition());
assertEquals("last fully visible child should be at position",
4, mRecyclerView.getChildViewHolder(mLayoutManager.
findFirstVisibleItemClosestToEnd(true)).getPosition());
assertEquals("first visible child should be at position",
0, mRecyclerView.getChildViewHolder(mLayoutManager.
findFirstVisibleItemClosestToStart(false)).getPosition());
assertEquals("last visible child should be at position",
4, mRecyclerView.getChildViewHolder(mLayoutManager.
findFirstVisibleItemClosestToEnd(false)).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) {
@NonNull
@Override
public TestViewHolder onCreateViewHolder(
@NonNull 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
@SuppressWarnings("deprecated") // using this for kitkat tests
public void onBindViewHolder(@NonNull TestViewHolder holder, int position) {
Item item = mItems.get(position);
holder.mBoundItem = item;
((EditText) ((FrameLayout) holder.itemView).getChildAt(0)).setText(
item.getDisplayText());
// 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));
holder.itemView.setBackgroundDrawable(stl);
if (mOnBindCallback != null) {
mOnBindCallback.onBoundItem(holder, position);
}
}
});
mLayoutManager.expectLayouts(1);
setRecyclerView(mRecyclerView);
mLayoutManager.waitForLayout(10);
getInstrumentation().waitForIdleSync();
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.getAbsoluteAdapterPosition() + " vs "
+ containingViewHolder.getAbsoluteAdapterPosition(),
lastViewHolder.getAbsoluteAdapterPosition()
< containingViewHolder.getAbsoluteAdapterPosition());
}
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
@SuppressWarnings("deprecated") // using this for kitkat tests
public TestViewHolder onCreateViewHolder(@NonNull 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.setBackgroundDrawable(stl);
return testViewHolder;
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
mAttachedRv = recyclerView;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
holder.itemView.getLayoutParams().height = 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).getAbsoluteAdapterPosition());
pos += 3;
}
for (int i : new int[]{18, 19, 20, 21, 23, 24}) {
focusSearchAndWaitForScroll(focusedView, focusDir);
focusedView = mRecyclerView.getFocusedChild();
assertEquals(i, mRecyclerView.getChildViewHolder(
focusedView).getAbsoluteAdapterPosition());
}
// now move right
focusSearch(focusedView, View.FOCUS_RIGHT);
waitForIdleScroll(mRecyclerView);
focusedView = mRecyclerView.getFocusedChild();
assertEquals(25,
mRecyclerView.getChildViewHolder(focusedView).getAbsoluteAdapterPosition());
for (int i : new int[]{28, 30}) {
focusSearchAndWaitForScroll(focusedView, focusDir);
focusedView = mRecyclerView.getFocusedChild();
assertEquals(i, mRecyclerView.getChildViewHolder(
focusedView).getAbsoluteAdapterPosition());
}
}
private void focusSearchAndWaitForScroll(View focused, int dir) throws Throwable {
focusSearch(focused, dir);
waitForIdleScroll(mRecyclerView);
}
@Test
public void topUnfocusableViewsVisibility() throws Throwable {
// The maximum number of rows that can be fully in-bounds of RV.
final int visibleRowCount = 5;
final int spanCount = 3;
final int lastFocusableIndex = 6;
setupByConfig(new Config(VERTICAL, true, spanCount, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
new GridTestAdapter(18, 1) {
RecyclerView mAttachedRv;
@Override
@SuppressWarnings("deprecated") // using this for kitkat tests
public TestViewHolder onCreateViewHolder(@NonNull 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.setBackgroundDrawable(stl);
return testViewHolder;
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
mAttachedRv = recyclerView;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
.getLayoutParams();
if (position <= lastFocusableIndex) {
holder.itemView.setFocusable(true);
holder.itemView.setFocusableInTouchMode(true);
} else {
holder.itemView.setFocusable(false);
holder.itemView.setFocusableInTouchMode(false);
}
lp.height = mAttachedRv.getHeight() / visibleRowCount;
lp.topMargin = 0;
lp.leftMargin = 0;
lp.rightMargin = 0;
lp.bottomMargin = 0;
if (position == 11) {
lp.bottomMargin = 9;
}
}
});
/**
*
* 15 16 17
* 12 13 14
* 11 11 11
* 9 10
* 8 8 8
* 7
* 6 6 6
* 3 4 5
* 0 1 2
*/
mAdapter.mFullSpanItems.add(6);
mAdapter.mFullSpanItems.add(8);
mAdapter.mFullSpanItems.add(11);
waitFirstLayout();
// adapter position of the currently focused item.
int focusIndex = 1;
RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
focusIndex);
View viewToFocus = toFocus.itemView;
assertTrue(requestFocus(viewToFocus, true));
assertSame(viewToFocus, mRecyclerView.getFocusedChild());
// The VH of the unfocusable item that just became fully visible after focusSearch.
RecyclerView.ViewHolder toVisible = null;
View focusedView = viewToFocus;
int actualFocusIndex = -1;
// First, scroll until the last focusable row.
for (int i : new int[]{4, 6}) {
focusSearchAndWaitForScroll(focusedView, View.FOCUS_UP);
focusedView = mRecyclerView.getFocusedChild();
actualFocusIndex = mRecyclerView.getChildViewHolder(
focusedView).getAbsoluteAdapterPosition();
assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
+ actualFocusIndex, i, actualFocusIndex);
}
// Further scroll up in order to make the unfocusable rows visible. This process should
// continue until the currently focused item is still visible. The focused item should not
// change in this loop.
for (int i : new int[]{9, 11, 11, 11}) {
focusSearchAndWaitForScroll(focusedView, View.FOCUS_UP);
focusedView = mRecyclerView.getFocusedChild();
actualFocusIndex =
mRecyclerView.getChildViewHolder(
focusedView).getAbsoluteAdapterPosition();
toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals("Focused view should not be changed, whereas it's now at "
+ actualFocusIndex, 6, actualFocusIndex);
assertTrue("Focused child should be at least partially visible.",
isViewPartiallyInBound(mRecyclerView, focusedView));
assertTrue("Child view at adapter pos " + i + " should be fully visible.",
isViewFullyInBound(mRecyclerView, toVisible.itemView));
}
}
@Test
public void bottomUnfocusableViewsVisibility() throws Throwable {
// The maximum number of rows that can be fully in-bounds of RV.
final int visibleRowCount = 5;
final int spanCount = 3;
final int lastFocusableIndex = 6;
setupByConfig(new Config(VERTICAL, false, spanCount, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
new GridTestAdapter(18, 1) {
RecyclerView mAttachedRv;
@Override
@SuppressWarnings("deprecated") // using this for kitkat tests
public TestViewHolder onCreateViewHolder(@NonNull 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.setBackgroundDrawable(stl);
return testViewHolder;
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
mAttachedRv = recyclerView;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
.getLayoutParams();
if (position <= lastFocusableIndex) {
holder.itemView.setFocusable(true);
holder.itemView.setFocusableInTouchMode(true);
} else {
holder.itemView.setFocusable(false);
holder.itemView.setFocusableInTouchMode(false);
}
lp.height = mAttachedRv.getHeight() / visibleRowCount;
lp.topMargin = 0;
lp.leftMargin = 0;
lp.rightMargin = 0;
lp.bottomMargin = 0;
if (position == 11) {
lp.topMargin = 9;
}
}
});
/**
* 0 1 2
* 3 4 5
* 6 6 6
* 7
* 8 8 8
* 9 10
* 11 11 11
* 12 13 14
* 15 16 17
*/
mAdapter.mFullSpanItems.add(6);
mAdapter.mFullSpanItems.add(8);
mAdapter.mFullSpanItems.add(11);
waitFirstLayout();
// adapter position of the currently focused item.
int focusIndex = 1;
RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
focusIndex);
View viewToFocus = toFocus.itemView;
assertTrue(requestFocus(viewToFocus, true));
assertSame(viewToFocus, mRecyclerView.getFocusedChild());
// The VH of the unfocusable item that just became fully visible after focusSearch.
RecyclerView.ViewHolder toVisible = null;
View focusedView = viewToFocus;
int actualFocusIndex = -1;
// First, scroll until the last focusable row.
for (int i : new int[]{4, 6}) {
focusSearchAndWaitForScroll(focusedView, View.FOCUS_DOWN);
focusedView = mRecyclerView.getFocusedChild();
actualFocusIndex = mRecyclerView.getChildViewHolder(
focusedView).getAbsoluteAdapterPosition();
assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
+ actualFocusIndex, i, actualFocusIndex);
}
// Further scroll down in order to make the unfocusable rows visible. This process should
// continue until the currently focused item is still visible. The focused item should not
// change in this loop.
for (int i : new int[]{9, 11, 11, 11}) {
focusSearchAndWaitForScroll(focusedView, View.FOCUS_DOWN);
focusedView = mRecyclerView.getFocusedChild();
actualFocusIndex = mRecyclerView.getChildViewHolder(
focusedView).getAbsoluteAdapterPosition();
toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals("Focused view should not be changed, whereas it's now at "
+ actualFocusIndex, 6, actualFocusIndex);
assertTrue("Focused child should be at least partially visible.",
isViewPartiallyInBound(mRecyclerView, focusedView));
assertTrue("Child view at adapter pos " + i + " should be fully visible.",
isViewFullyInBound(mRecyclerView, toVisible.itemView));
}
}
@Test
public void leftUnfocusableViewsVisibility() throws Throwable {
// The maximum number of columns that can be fully in-bounds of RV.
final int visibleColCount = 5;
final int spanCount = 3;
final int lastFocusableIndex = 6;
final int childWidth = 200;
final int childHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
final int parentWidth = childWidth * visibleColCount;
final int parentHeight = 1000;
// Reverse layout so that views are placed from right to left.
setupByConfig(new Config(HORIZONTAL, true, spanCount,
GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
new GridTestAdapter(18, 1) {
@Override
@SuppressWarnings("deprecated") // using this for kitkat tests
public TestViewHolder onCreateViewHolder(@NonNull 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.setBackgroundDrawable(stl);
return testViewHolder;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
if (position <= lastFocusableIndex) {
holder.itemView.setFocusable(true);
holder.itemView.setFocusableInTouchMode(true);
} else {
holder.itemView.setFocusable(false);
holder.itemView.setFocusableInTouchMode(false);
}
StaggeredGridLayoutManager.LayoutParams oldLp =
(StaggeredGridLayoutManager.LayoutParams)
holder.itemView.getLayoutParams();
StaggeredGridLayoutManager.LayoutParams newLp =
new StaggeredGridLayoutManager.LayoutParams(
childWidth,
childHeight);
newLp.setFullSpan(oldLp.mFullSpan);
newLp.topMargin = 0;
newLp.leftMargin = 0;
newLp.rightMargin = 0;
newLp.bottomMargin = 0;
if (position == 11) {
newLp.leftMargin = 9;
}
holder.itemView.setLayoutParams(newLp);
}
});
/**
* 15 12 11 9 8 7 6 3 0
* 16 13 11 10 8 6 4 1
* 17 14 11 8 6 5 2
*/
mAdapter.mFullSpanItems.add(6);
mAdapter.mFullSpanItems.add(8);
mAdapter.mFullSpanItems.add(11);
mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(parentWidth, parentHeight));
waitFirstLayout();
// adapter position of the currently focused item.
int focusIndex = 1;
RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
focusIndex);
View viewToFocus = toFocus.itemView;
assertTrue(requestFocus(viewToFocus, true));
assertSame(viewToFocus, mRecyclerView.getFocusedChild());
// The VH of the unfocusable item that just became fully visible after focusSearch.
RecyclerView.ViewHolder toVisible = null;
View focusedView = viewToFocus;
int actualFocusIndex = -1;
// First, scroll until the last focusable column.
for (int i : new int[]{4, 6}) {
focusSearchAndWaitForScroll(focusedView, View.FOCUS_LEFT);
focusedView = mRecyclerView.getFocusedChild();
actualFocusIndex = mRecyclerView.getChildViewHolder(
focusedView).getAbsoluteAdapterPosition();
assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
+ actualFocusIndex, i, actualFocusIndex);
}
// Further scroll left in order to make the unfocusable columns visible. This process should
// continue until the currently focused item is still visible. The focused item should not
// change in this loop.
for (int i : new int[]{9, 11, 11, 11}) {
focusSearchAndWaitForScroll(focusedView, View.FOCUS_LEFT);
focusedView = mRecyclerView.getFocusedChild();
actualFocusIndex = mRecyclerView.getChildViewHolder(
focusedView).getAbsoluteAdapterPosition();
toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals("Focused view should not be changed, whereas it's now at "
+ actualFocusIndex, 6, actualFocusIndex);
assertTrue("Focused child should be at least partially visible.",
isViewPartiallyInBound(mRecyclerView, focusedView));
assertTrue("Child view at adapter pos " + i + " should be fully visible.",
isViewFullyInBound(mRecyclerView, toVisible.itemView));
}
}
@Test
public void rightUnfocusableViewsVisibility() throws Throwable {
// The maximum number of columns that can be fully in-bounds of RV.
final int visibleColCount = 5;
final int spanCount = 3;
final int lastFocusableIndex = 6;
final int childWidth = 200;
final int childHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
final int parentWidth = childWidth * visibleColCount;
final int parentHeight = 1000;
setupByConfig(new Config(HORIZONTAL, false, spanCount,
GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
new GridTestAdapter(18, 1) {
@Override
@SuppressWarnings("deprecated") // using this for kitkat tests
public TestViewHolder onCreateViewHolder(@NonNull 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.setBackgroundDrawable(stl);
return testViewHolder;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
if (position <= lastFocusableIndex) {
holder.itemView.setFocusable(true);
holder.itemView.setFocusableInTouchMode(true);
} else {
holder.itemView.setFocusable(false);
holder.itemView.setFocusableInTouchMode(false);
}
StaggeredGridLayoutManager.LayoutParams oldLp =
(StaggeredGridLayoutManager.LayoutParams)
holder.itemView.getLayoutParams();
StaggeredGridLayoutManager.LayoutParams newLp =
new StaggeredGridLayoutManager.LayoutParams(
childWidth,
childHeight);
newLp.setFullSpan(oldLp.mFullSpan);
newLp.topMargin = 0;
newLp.leftMargin = 0;
newLp.rightMargin = 0;
newLp.bottomMargin = 0;
if (position == 11) {
newLp.leftMargin = 9;
}
holder.itemView.setLayoutParams(newLp);
}
});
/**
* 0 3 6 7 8 9 11 12 15
* 1 4 6 8 10 11 13 16
* 2 5 6 8 11 14 17
*/
mAdapter.mFullSpanItems.add(6);
mAdapter.mFullSpanItems.add(8);
mAdapter.mFullSpanItems.add(11);
mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(parentWidth, parentHeight));
waitFirstLayout();
// adapter position of the currently focused item.
int focusIndex = 1;
RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
focusIndex);
View viewToFocus = toFocus.itemView;
assertTrue(requestFocus(viewToFocus, true));
assertSame(viewToFocus, mRecyclerView.getFocusedChild());
// The VH of the unfocusable item that just became fully visible after focusSearch.
RecyclerView.ViewHolder toVisible = null;
View focusedView = viewToFocus;
int actualFocusIndex = -1;
// First, scroll until the last focusable column.
for (int i : new int[]{4, 6}) {
focusSearchAndWaitForScroll(focusedView, View.FOCUS_RIGHT);
focusedView = mRecyclerView.getFocusedChild();
actualFocusIndex = mRecyclerView.getChildViewHolder(
focusedView).getAbsoluteAdapterPosition();
assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
+ actualFocusIndex, i, actualFocusIndex);
}
// Further scroll right in order to make the unfocusable rows visible. This process should
// continue until the currently focused item is still visible. The focused item should not
// change in this loop.
for (int i : new int[]{9, 11, 11, 11}) {
focusSearchAndWaitForScroll(focusedView, View.FOCUS_RIGHT);
focusedView = mRecyclerView.getFocusedChild();
actualFocusIndex = mRecyclerView.getChildViewHolder(
focusedView).getAbsoluteAdapterPosition();
toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals("Focused view should not be changed, whereas it's now at "
+ actualFocusIndex, 6, actualFocusIndex);
assertTrue("Focused child should be at least partially visible.",
isViewPartiallyInBound(mRecyclerView, focusedView));
assertTrue("Child view at adapter pos " + i + " should be fully visible.",
isViewFullyInBound(mRecyclerView, toVisible.itemView));
}
}
@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);
mActivityRule.runOnUiThread(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 testBatchInsertionsBetweenTailFullSpanItems() throws Throwable {
// Magic numbers here aren't super specific to repro, but were the example test case that
// led to the isolation of this bug.
setupByConfig(new Config().spanCount(2).itemCount(22));
// Last few items are full spans. Create a variable to reference later, even though it's
// basically just a few repeated calls.
mAdapter.mFullSpanItems.add(18);
mAdapter.mFullSpanItems.add(19);
mAdapter.mFullSpanItems.add(20);
mAdapter.mFullSpanItems.add(21);
waitFirstLayout();
// Scroll to the end to populate full span items.
smoothScrollToPosition(mAdapter.mItems.size() - 1);
// Incrementally add a handful of items, mimicking some adapter usages.
final int numberOfItemsToAdd = 12;
final int fullSpanItemIndexToInsertFrom = 18 + 1;
for (int i = 0; i < numberOfItemsToAdd; i++) {
final int insertAt = fullSpanItemIndexToInsertFrom + i;
mAdapter.addAndNotify(insertAt, 1);
}
requestLayoutOnUIThread(mRecyclerView);
mLayoutManager.waitForLayout(3);
}
@Test
public void temporaryGapHandling() throws Throwable {
int fullSpanIndex = 100;
setupByConfig(new Config()
.spanCount(2)
.itemCount(250)
.recyclerViewLayoutWidth(800)
.recyclerViewLayoutHeight(1600));
mAdapter.mFullSpanItems.add(fullSpanIndex);
waitFirstLayout();
smoothScrollToPosition(fullSpanIndex + 100); // 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);
}
@Test
public void innerGapHandlingMoveItemsBetweenSpans() throws Throwable {
innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
}
private 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:
// Wait time is 10 seconds because 2 seconds appeared to be flaky. If test still
// flakes with this, there must be another problem and further investigation will be
// needed.
mLayoutManager.waitForLayout(10);
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);
if (RecyclerView.POST_UPDATES_ON_ANIMATION) {
mAdapter.changeAndNotify(changePosition, 1);
mLayoutManager.assertNoLayout("no layout should happen when an invisible child is "
+ "updated", 1);
} else {
mAdapter.changeAndNotify(changePosition, 1);
mLayoutManager.waitForLayout(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).itemCount(50);
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 SGLM 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);
mActivityRule.runOnUiThread(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();
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
}
});
final int start = mRecyclerView
.getChildLayoutPosition(
mLayoutManager.findFirstVisibleItemClosestToStart(false));
final int end = mRecyclerView
.getChildLayoutPosition(
mLayoutManager.findFirstVisibleItemClosestToEnd(false));
assertEquals("first item position should match",
Math.min(start, end), event.getFromIndex());
assertEquals("last item position should match",
Math.max(start, end), event.getToIndex());
}
}