blob: f0225d36ba083c0c7e407b9f559dc15bdc44f264 [file] [log] [blame]
/*
* Copyright (C) 2016 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 org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.os.Build;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SdkSuppress;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.View;
import android.view.ViewGroup;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.List;
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
@RunWith(AndroidJUnit4.class)
public class RecyclerViewCacheTest {
RecyclerView mRecyclerView;
RecyclerView.Recycler mRecycler;
RecyclerView.ViewPrefetcher mViewPrefetcher;
@Before
public void setUp() throws Exception {
mRecyclerView = new RecyclerView(getContext());
mRecycler = mRecyclerView.mRecycler;
mViewPrefetcher = mRecyclerView.mViewPrefetcher;
}
private Context getContext() {
return InstrumentationRegistry.getContext();
}
@Test
public void prefetchReusesCacheItems() {
RecyclerView.LayoutManager prefetchingLayoutManager = new RecyclerView.LayoutManager() {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
int getItemPrefetchCount() {
return 3;
}
@Override
int gatherPrefetchIndices(int dx, int dy, RecyclerView.State state, int[] outIndices) {
outIndices[0] = 0;
outIndices[1] = 1;
outIndices[2] = 2;
return 3;
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
}
};
mRecyclerView.setLayoutManager(prefetchingLayoutManager);
RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
.thenAnswer(new Answer<RecyclerView.ViewHolder>() {
@Override
public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
throws Throwable {
return new RecyclerView.ViewHolder(new View(getContext())) {};
}
});
when(mockAdapter.getItemCount()).thenReturn(10);
mRecyclerView.setAdapter(mockAdapter);
mRecyclerView.measure(View.MeasureSpec.AT_MOST | 320, View.MeasureSpec.AT_MOST | 240);
mRecyclerView.layout(0, 0, 320, 320);
verify(mockAdapter, never()).onCreateViewHolder(any(ViewGroup.class), anyInt());
verify(mockAdapter, never()).onBindViewHolder(
any(RecyclerView.ViewHolder.class), anyInt(), any(List.class));
assertTrue(mRecycler.mCachedViews.isEmpty());
// Prefetch multiple times...
for (int i = 0; i < 4; i++) {
int[] itemPrefetchArray = new int[] {-1, -1, -1};
int viewCount = prefetchingLayoutManager.gatherPrefetchIndices(1, 1,
mRecyclerView.mState, itemPrefetchArray);
mRecycler.prefetch(itemPrefetchArray, viewCount);
// ...but should only see the same three items fetched/bound once each
verify(mockAdapter, times(3)).onCreateViewHolder(any(ViewGroup.class), anyInt());
verify(mockAdapter, times(3)).onBindViewHolder(
any(RecyclerView.ViewHolder.class), anyInt(), any(List.class));
assertTrue(mRecycler.mCachedViews.size() == 3);
verifyCacheContainsPositions(0, 1, 2);
}
}
private void verifyCacheContainsPosition(int position) {
for (int i = 0; i < mRecycler.mCachedViews.size(); i++) {
if (mRecycler.mCachedViews.get(i).mPosition == position) return;
}
fail("Cache does not contain position " + position);
}
private void verifyCacheContainsPositions(Integer... positions) {
for (int i = 0; i < positions.length; i++) {
verifyCacheContainsPosition(positions[i]);
}
}
@Test
public void prefetchItemsNotEvictedWithInserts() {
mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));
RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
.thenAnswer(new Answer<RecyclerView.ViewHolder>() {
@Override
public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
throws Throwable {
return new RecyclerView.ViewHolder(new View(getContext())) {};
}
});
when(mockAdapter.getItemCount()).thenReturn(100);
mRecyclerView.setAdapter(mockAdapter);
mRecyclerView.measure(View.MeasureSpec.AT_MOST | 320, View.MeasureSpec.AT_MOST | 320);
mRecyclerView.layout(0, 0, 320, 320);
mViewPrefetcher.mItemPrefetchArray = new int[] { 0, 1, 2 };
mRecycler.prefetch(mViewPrefetcher.mItemPrefetchArray, 3);
verifyCacheContainsPositions(0, 1, 2);
// further views recycled, as though from scrolling, shouldn't evict prefetched views:
mRecycler.recycleView(mRecycler.getViewForPosition(10));
verifyCacheContainsPositions(0, 1, 2, 10);
mRecycler.recycleView(mRecycler.getViewForPosition(20));
verifyCacheContainsPositions(0, 1, 2, 10, 20);
mRecycler.recycleView(mRecycler.getViewForPosition(30));
verifyCacheContainsPositions(0, 1, 2, 20, 30);
mRecycler.recycleView(mRecycler.getViewForPosition(40));
verifyCacheContainsPositions(0, 1, 2, 30, 40);
// After clearing the cache, the prefetch priorities should be cleared as well:
mRecyclerView.mRecycler.recycleAndClearCachedViews();
for (int i : new int[] {0, 1, 2, 50, 60, 70, 80, 90}) {
mRecycler.recycleView(mRecycler.getViewForPosition(i));
}
// cache only contains most recent positions, no priority for previous prefetches:
verifyCacheContainsPositions(50, 60, 70, 80, 90);
}
@Test
public void prefetchItemsNotEvictedOnScroll() {
mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));
// 100x100 pixel views
RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
.thenAnswer(new Answer<RecyclerView.ViewHolder>() {
@Override
public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
throws Throwable {
View view = new View(getContext());
view.setMinimumWidth(100);
view.setMinimumHeight(100);
return new RecyclerView.ViewHolder(view) {};
}
});
when(mockAdapter.getItemCount()).thenReturn(100);
mRecyclerView.setAdapter(mockAdapter);
// NOTE: requested cache size must be smaller than span count so two rows cannot fit
mRecyclerView.setItemViewCacheSize(2);
mRecyclerView.measure(View.MeasureSpec.AT_MOST | 300, View.MeasureSpec.AT_MOST | 200);
mRecyclerView.layout(0, 0, 300, 150);
mRecyclerView.scrollBy(0, 75);
assertTrue(mRecycler.mCachedViews.isEmpty());
// rows 0, 1, and 2 are all attached and visible. Prefetch row 3:
mViewPrefetcher.mItemPrefetchArray = new int[] {-1, -1, -1};
int viewCount = mRecyclerView.getLayoutManager().gatherPrefetchIndices(0, 1,
mRecyclerView.mState, mViewPrefetcher.mItemPrefetchArray);
mRecycler.prefetch(mViewPrefetcher.mItemPrefetchArray, viewCount);
// row 3 is cached:
verifyCacheContainsPositions(9, 10, 11);
assertTrue(mRecycler.mCachedViews.size() == 3);
// Scroll so 1 falls off (though 3 is still not on screen)
mRecyclerView.scrollBy(0, 50);
// row 3 is still cached, with a couple other recycled views:
verifyCacheContainsPositions(9, 10, 11);
assertTrue(mRecycler.mCachedViews.size() == 5);
}
}