/* * 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.HORIZONTAL; import static android.support.v7.widget.LinearLayoutManager.VERTICAL; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; 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.drawable.ColorDrawable; import android.graphics.drawable.StateListDrawable; import android.os.Build; import android.support.test.filters.MediumTest; import android.support.test.filters.SdkSuppress; import android.support.test.runner.AndroidJUnit4; import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.test.UiThreadTest; import android.util.SparseIntArray; import android.util.StateSet; import android.view.View; import android.view.ViewGroup; import org.hamcrest.CoreMatchers; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @MediumTest @RunWith(AndroidJUnit4.class) public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { @Test public void focusSearchFailureUp() throws Throwable { focusSearchFailure(false); } @Test public void focusSearchFailureDown() throws Throwable { focusSearchFailure(true); } @Test public void scrollToBadOffset() throws Throwable { scrollToBadOffset(false); } @Test public void scrollToBadOffsetReverse() throws Throwable { scrollToBadOffset(true); } private void scrollToBadOffset(boolean reverseLayout) throws Throwable { final int w = 500; final int h = 1000; RecyclerView recyclerView = setupBasic(new Config(2, 100).reverseLayout(reverseLayout), new GridTestAdapter(100) { @Override public void onBindViewHolder(TestViewHolder holder, int position) { super.onBindViewHolder(holder, position); ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); if (lp == null) { lp = new ViewGroup.LayoutParams(w / 2, h / 2); holder.itemView.setLayoutParams(lp); } else { lp.width = w / 2; lp.height = h / 2; holder.itemView.setLayoutParams(lp); } } }); TestedFrameLayout.FullControlLayoutParams lp = new TestedFrameLayout.FullControlLayoutParams(w, h); recyclerView.setLayoutParams(lp); waitForFirstLayout(recyclerView); mGlm.expectLayout(1); scrollToPosition(11); mGlm.waitForLayout(2); // assert spans and position etc for (int i = 0; i < mGlm.getChildCount(); i++) { View child = mGlm.getChildAt(i); GridLayoutManager.LayoutParams params = (GridLayoutManager.LayoutParams) child .getLayoutParams(); assertThat("span index for child at " + i + " with position " + params .getViewAdapterPosition(), params.getSpanIndex(), CoreMatchers.is(params.getViewAdapterPosition() % 2)); } // assert spans and positions etc. int lastVisible = mGlm.findLastVisibleItemPosition(); // this should be the scrolled child assertThat(lastVisible, CoreMatchers.is(11)); } private void focusSearchFailure(boolean scrollDown) throws Throwable { final RecyclerView recyclerView = setupBasic(new Config(3, 31).reverseLayout(!scrollDown) , 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)); //noinspection deprecation using this for kitkat tests testViewHolder.itemView.setBackgroundDrawable(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); } }); waitForFirstLayout(recyclerView); View viewToFocus = recyclerView.findViewHolderForAdapterPosition(1).itemView; assertTrue(requestFocus(viewToFocus, true)); assertSame(viewToFocus, recyclerView.getFocusedChild()); int pos = 1; View focusedView = viewToFocus; while (pos < 31) { focusSearch(focusedView, scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP); waitForIdleScroll(recyclerView); focusedView = recyclerView.getFocusedChild(); assertEquals(Math.min(pos + 3, mAdapter.getItemCount() - 1), recyclerView.getChildViewHolder(focusedView).getAdapterPosition()); pos += 3; } } @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 consecutiveFocusableRowsCount = 4; final int consecutiveUnFocusableRowsCount = 8; final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount) * spanCount; final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount) .reverseLayout(true), new GridTestAdapter(itemCount, 1) { RecyclerView mAttachedRv; @Override public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); // 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)); //noinspection deprecation using this for kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { mAttachedRv = recyclerView; } @Override public void onBindViewHolder(TestViewHolder holder, int position) { super.onBindViewHolder(holder, position); if (position < spanCount * consecutiveFocusableRowsCount) { holder.itemView.setFocusable(true); holder.itemView.setFocusableInTouchMode(true); } else { holder.itemView.setFocusable(false); holder.itemView.setFocusableInTouchMode(false); } holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount); } }); waitForFirstLayout(recyclerView); // adapter position of the currently focused item. int focusIndex = 1; RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); View viewToFocus = toFocus.itemView; assertTrue(requestFocus(viewToFocus, true)); assertSame(viewToFocus, recyclerView.getFocusedChild()); // adapter position of the item (whether focusable or not) that just becomes fully // visible after focusSearch. int visibleIndex = focusIndex; // The VH of the above adapter position RecyclerView.ViewHolder toVisible = null; int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex; int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2) * spanCount + visibleIndex; // Navigate up through the focusable and unfocusable rows. The focusable rows should // become focused one by one until hitting the last focusable row, at which point, // unfocusable rows should become visible on the screen until the currently focused row // stays on the screen. int pos = focusIndex + spanCount; while (pos < itemCount) { focusSearch(recyclerView.getFocusedChild(), View.FOCUS_UP, true); waitForIdleScroll(recyclerView); focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount)); toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount)); toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex); assertThat("Child at position " + focusIndex + " should be focused", toFocus.itemView.hasFocus(), is(true)); assertTrue("Focused child should be at least partially visible.", isViewPartiallyInBound(recyclerView, toFocus.itemView)); assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", isViewFullyInBound(recyclerView, toVisible.itemView)); pos += spanCount; } } @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 consecutiveFocusableRowsCount = 4; final int consecutiveUnFocusableRowsCount = 8; final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount) * spanCount; final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount) .reverseLayout(false), new GridTestAdapter(itemCount, 1) { RecyclerView mAttachedRv; @Override public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); // 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)); //noinspection deprecation using this for kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { mAttachedRv = recyclerView; } @Override public void onBindViewHolder(TestViewHolder holder, int position) { super.onBindViewHolder(holder, position); if (position < spanCount * consecutiveFocusableRowsCount) { holder.itemView.setFocusable(true); holder.itemView.setFocusableInTouchMode(true); } else { holder.itemView.setFocusable(false); holder.itemView.setFocusableInTouchMode(false); } holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount); } }); waitForFirstLayout(recyclerView); // adapter position of the currently focused item. int focusIndex = 1; RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); View viewToFocus = toFocus.itemView; assertTrue(requestFocus(viewToFocus, true)); assertSame(viewToFocus, recyclerView.getFocusedChild()); // adapter position of the item (whether focusable or not) that just becomes fully // visible after focusSearch. int visibleIndex = focusIndex; // The VH of the above adapter position RecyclerView.ViewHolder toVisible = null; int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex; int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2) * spanCount + visibleIndex; // Navigate down through the focusable and unfocusable rows. The focusable rows should // become focused one by one until hitting the last focusable row, at which point, // unfocusable rows should become visible on the screen until the currently focused row // stays on the screen. int pos = focusIndex + spanCount; while (pos < itemCount) { focusSearch(recyclerView.getFocusedChild(), View.FOCUS_DOWN, true); waitForIdleScroll(recyclerView); focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount)); toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount)); toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex); assertThat("Child at position " + focusIndex + " should be focused", toFocus.itemView.hasFocus(), is(true)); assertTrue("Focused child should be at least partially visible.", isViewPartiallyInBound(recyclerView, toFocus.itemView)); assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", isViewFullyInBound(recyclerView, toVisible.itemView)); pos += spanCount; } } @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 consecutiveFocusableColsCount = 4; final int consecutiveUnFocusableColsCount = 8; final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount) * spanCount; final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount) .orientation(HORIZONTAL).reverseLayout(true), new GridTestAdapter(itemCount, 1) { RecyclerView mAttachedRv; @Override public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); // 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)); //noinspection deprecation using this for kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { mAttachedRv = recyclerView; } @Override public void onBindViewHolder(TestViewHolder holder, int position) { super.onBindViewHolder(holder, position); if (position < spanCount * consecutiveFocusableColsCount) { holder.itemView.setFocusable(true); holder.itemView.setFocusableInTouchMode(true); } else { holder.itemView.setFocusable(false); holder.itemView.setFocusableInTouchMode(false); } holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount); } }); waitForFirstLayout(recyclerView); // adapter position of the currently focused item. int focusIndex = 1; RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); View viewToFocus = toFocus.itemView; assertTrue(requestFocus(viewToFocus, true)); assertSame(viewToFocus, recyclerView.getFocusedChild()); // adapter position of the item (whether focusable or not) that just becomes fully // visible after focusSearch. int visibleIndex = focusIndex; // The VH of the above adapter position RecyclerView.ViewHolder toVisible = null; int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex; int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2) * spanCount + visibleIndex; // Navigate left through the focusable and unfocusable columns. The focusable columns should // become focused one by one until hitting the last focusable column, at which point, // unfocusable columns should become visible on the screen until the currently focused // column stays on the screen. int pos = focusIndex + spanCount; while (pos < itemCount) { focusSearch(recyclerView.getFocusedChild(), View.FOCUS_LEFT, true); waitForIdleScroll(recyclerView); focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount)); toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount)); toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex); assertThat("Child at position " + focusIndex + " should be focused", toFocus.itemView.hasFocus(), is(true)); assertTrue("Focused child should be at least partially visible.", isViewPartiallyInBound(recyclerView, toFocus.itemView)); assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", isViewFullyInBound(recyclerView, toVisible.itemView)); pos += spanCount; } } @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 consecutiveFocusableColsCount = 4; final int consecutiveUnFocusableColsCount = 8; final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount) * spanCount; final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount) .orientation(HORIZONTAL).reverseLayout(false), new GridTestAdapter(itemCount, 1) { RecyclerView mAttachedRv; @Override public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); // 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)); //noinspection deprecation using this for kitkat tests testViewHolder.itemView.setBackgroundDrawable(stl); return testViewHolder; } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { mAttachedRv = recyclerView; } @Override public void onBindViewHolder(TestViewHolder holder, int position) { super.onBindViewHolder(holder, position); if (position < spanCount * consecutiveFocusableColsCount) { holder.itemView.setFocusable(true); holder.itemView.setFocusableInTouchMode(true); } else { holder.itemView.setFocusable(false); holder.itemView.setFocusableInTouchMode(false); } holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount); } }); waitForFirstLayout(recyclerView); // adapter position of the currently focused item. int focusIndex = 1; RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); View viewToFocus = toFocus.itemView; assertTrue(requestFocus(viewToFocus, true)); assertSame(viewToFocus, recyclerView.getFocusedChild()); // adapter position of the item (whether focusable or not) that just becomes fully // visible after focusSearch. int visibleIndex = focusIndex; // The VH of the above adapter position RecyclerView.ViewHolder toVisible = null; int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex; int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2) * spanCount + visibleIndex; // Navigate right through the focusable and unfocusable columns. The focusable columns // should become focused one by one until hitting the last focusable column, at which point, // unfocusable columns should become visible on the screen until the currently focused // column stays on the screen. int pos = focusIndex + spanCount; while (pos < itemCount) { focusSearch(recyclerView.getFocusedChild(), View.FOCUS_RIGHT, true); waitForIdleScroll(recyclerView); focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount)); toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount)); toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex); assertThat("Child at position " + focusIndex + " should be focused", toFocus.itemView.hasFocus(), is(true)); assertTrue("Focused child should be at least partially visible.", isViewPartiallyInBound(recyclerView, toFocus.itemView)); assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", isViewFullyInBound(recyclerView, toVisible.itemView)); pos += spanCount; } } @UiThreadTest @Test public void scrollWithoutLayout() throws Throwable { final RecyclerView recyclerView = setupBasic(new Config(3, 100)); mGlm.expectLayout(1); setRecyclerView(recyclerView); mGlm.setSpanCount(5); recyclerView.scrollBy(0, 10); } @Test public void scrollWithoutLayoutAfterInvalidate() throws Throwable { final RecyclerView recyclerView = setupBasic(new Config(3, 100)); waitForFirstLayout(recyclerView); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mGlm.setSpanCount(5); recyclerView.scrollBy(0, 10); } }); } @Test public void predictiveSpanLookup1() throws Throwable { predictiveSpanLookupTest(0, false); } @Test public void predictiveSpanLookup2() throws Throwable { predictiveSpanLookupTest(0, true); } @Test public void predictiveSpanLookup3() throws Throwable { predictiveSpanLookupTest(1, false); } @Test public void predictiveSpanLookup4() 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(); } @Test public void movingAGroupOffScreenForAddedItems() 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; } }); ((SimpleItemAnimator) 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)); } @Test public void layoutParams() throws Throwable { layoutParamsTest(GridLayoutManager.HORIZONTAL); removeRecyclerView(); layoutParamsTest(GridLayoutManager.VERTICAL); } @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void horizontalAccessibilitySpanIndices() throws Throwable { accessibilitySpanIndicesTest(HORIZONTAL); } @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void verticalAccessibilitySpanIndices() 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); mActivityRule.runOnUiThread(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.MATCH_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))); } @Test public void anchorUpdate() 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, LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition); glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); assertEquals("gm should keep anchor in last span in the row", 20, glm.mAnchorInfo.mPosition); glm.mAnchorInfo.mPosition = 5; glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); assertEquals("gm should keep anchor in last span in the row", 10, glm.mAnchorInfo.mPosition); glm.mAnchorInfo.mPosition = 13; glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition); glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); assertEquals("gm should keep anchor in last span in the row", 20, glm.mAnchorInfo.mPosition); glm.mAnchorInfo.mPosition = 23; glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition); glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); assertEquals("gm should keep anchor in last span in the row", 25, glm.mAnchorInfo.mPosition); glm.mAnchorInfo.mPosition = 35; glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition); glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); assertEquals("gm should keep anchor in last span in the row", 35, glm.mAnchorInfo.mPosition); } @Test public void spanLookup() { spanLookupTest(false); } @Test public void spanLookupWithCache() { spanLookupTest(true); } @Test public void spanLookupCache() { 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)); } @Test public void removeAnchorItem() throws Throwable { removeAnchorItemTest( new Config(3, 0).orientation(VERTICAL).reverseLayout(false), 100, 0); } @Test public void removeAnchorItemReverse() throws Throwable { removeAnchorItemTest( new Config(3, 0).orientation(VERTICAL).reverseLayout(true), 100, 0); } @Test public void removeAnchorItemHorizontal() throws Throwable { removeAnchorItemTest( new Config(3, 0).orientation(HORIZONTAL).reverseLayout( false), 100, 0); } @Test public void removeAnchorItemReverseHorizontal() 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.MATCH_PARENT; } else { maxSize = mRecyclerView.getHeight(); mlp.width = ViewGroup.MarginLayoutParams.MATCH_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); } @Test public void spanGroupIndex() { 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)); } @Test public void notifyDataSetChange() 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(); } @Test public void unevenHeights() 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()); } } @Test public void unevenWidths() 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()); } } @Test public void spanSizeChange() throws Throwable { final RecyclerView rv = setupBasic(new Config(3, 100)); waitForFirstLayout(rv); assertTrue(mGlm.supportsPredictiveItemAnimations()); mGlm.expectLayout(1); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mGlm.setSpanCount(5); assertFalse(mGlm.supportsPredictiveItemAnimations()); } }); mGlm.waitForLayout(2); mGlm.expectLayout(2); mAdapter.deleteAndNotify(3, 2); mGlm.waitForLayout(2); assertTrue(mGlm.supportsPredictiveItemAnimations()); } @Test public void cacheSpanIndices() 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()); } }