| /* |
| * 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.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.support.test.runner.AndroidJUnit4; |
| import android.support.v4.view.AccessibilityDelegateCompat; |
| import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; |
| import android.test.UiThreadTest; |
| import android.test.suitebuilder.annotation.MediumTest; |
| 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)); |
| 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); |
| } |
| }); |
| 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; |
| } |
| } |
| |
| @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); |
| runTestOnUiThread(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 |
| public void horizontalAccessibilitySpanIndices() throws Throwable { |
| accessibilitySpanIndicesTest(HORIZONTAL); |
| } |
| |
| @Test |
| 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); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info); |
| } |
| }); |
| GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup; |
| AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info |
| .getCollectionItemInfo(); |
| assertNotNull(itemInfo); |
| assertEquals("result should have span group position", |
| ssl.getSpanGroupIndex(position, mGlm.getSpanCount()), |
| orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex()); |
| assertEquals("result should have span index", |
| ssl.getSpanIndex(position, mGlm.getSpanCount()), |
| orientation == HORIZONTAL ? itemInfo.getRowIndex() : itemInfo.getColumnIndex()); |
| assertEquals("result should have span size", |
| ssl.getSpanSize(position), |
| orientation == HORIZONTAL ? itemInfo.getRowSpan() : itemInfo.getColumnSpan()); |
| } |
| |
| public GridLayoutManager.LayoutParams ensureGridLp(View view) { |
| ViewGroup.LayoutParams lp = view.getLayoutParams(); |
| GridLayoutManager.LayoutParams glp; |
| if (lp instanceof GridLayoutManager.LayoutParams) { |
| glp = (GridLayoutManager.LayoutParams) lp; |
| } else if (lp == null) { |
| glp = (GridLayoutManager.LayoutParams) mGlm |
| .generateDefaultLayoutParams(); |
| view.setLayoutParams(glp); |
| } else { |
| glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp); |
| view.setLayoutParams(glp); |
| } |
| return glp; |
| } |
| |
| public void layoutParamsTest(final int orientation) throws Throwable { |
| final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation), |
| new GridTestAdapter(100) { |
| @Override |
| public void onBindViewHolder(TestViewHolder holder, |
| int position) { |
| super.onBindViewHolder(holder, position); |
| GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView); |
| int val = 0; |
| switch (position % 5) { |
| case 0: |
| val = 10; |
| break; |
| case 1: |
| val = 30; |
| break; |
| case 2: |
| val = GridLayoutManager.LayoutParams.WRAP_CONTENT; |
| break; |
| case 3: |
| val = GridLayoutManager.LayoutParams.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); |
| runTestOnUiThread(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()); |
| } |
| } |