package android.support.v17.leanback.widget; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.mock; 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.Parcelable; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; @SmallTest @RunWith(AndroidJUnit4.class) public class GridWidgetPrefetchTest { private Context getContext() { return InstrumentationRegistry.getContext(); } private void layout(View view, int width, int height) { view.measure( View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)); view.layout(0, 0, width, height); } public void validatePrefetch(BaseGridView gridView, int scrollX, int scrollY, Integer[]... positionData) { // duplicates logic in support.v7.widget.CacheUtils#verifyPositionsPrefetched RecyclerView.State state = mock(RecyclerView.State.class); when(state.getItemCount()).thenReturn(gridView.getAdapter().getItemCount()); RecyclerView.LayoutManager.LayoutPrefetchRegistry registry = mock(RecyclerView.LayoutManager.LayoutPrefetchRegistry.class); gridView.getLayoutManager().collectAdjacentPrefetchPositions(scrollX, scrollY, state, registry); verify(registry, times(positionData.length)).addPosition(anyInt(), anyInt()); for (Integer[] aPositionData : positionData) { verify(registry).addPosition(aPositionData[0], aPositionData[1]); } } private RecyclerView.Adapter createBoxAdapter() { return new RecyclerView.Adapter() { @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = new View(getContext()); view.setMinimumWidth(100); view.setMinimumHeight(100); return new RecyclerView.ViewHolder(view) {}; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { // noop } @Override public int getItemCount() { return 100; } }; } @Test public void prefetch() { HorizontalGridView gridView = new HorizontalGridView(getContext()); gridView.setNumRows(1); gridView.setRowHeight(100); gridView.setAdapter(createBoxAdapter()); layout(gridView, 150, 100); // validate 2 children in viewport assertEquals(2, gridView.getChildCount()); assertEquals(0, gridView.getLayoutManager().findViewByPosition(0).getLeft()); assertEquals(100, gridView.getLayoutManager().findViewByPosition(1).getLeft()); validatePrefetch(gridView, -50, 0); // no view to left validatePrefetch(gridView, 50, 0, new Integer[] {2, 50}); // next view 50 pixels to right // scroll to position 5, and layout gridView.scrollToPosition(5); layout(gridView, 150, 100); /* Visual representation, each number column represents 25 pixels: * | | * ... 3 3 4 4 4|4 5 5 5 5 6|6 6 6 7 7 ... * | | */ // validate the 3 children in the viewport, and their positions assertEquals(3, gridView.getChildCount()); assertNotNull(gridView.getLayoutManager().findViewByPosition(4)); assertNotNull(gridView.getLayoutManager().findViewByPosition(5)); assertNotNull(gridView.getLayoutManager().findViewByPosition(6)); assertEquals(-75, gridView.getLayoutManager().findViewByPosition(4).getLeft()); assertEquals(25, gridView.getLayoutManager().findViewByPosition(5).getLeft()); assertEquals(125, gridView.getLayoutManager().findViewByPosition(6).getLeft()); // next views are 75 pixels to right and left: validatePrefetch(gridView, -50, 0, new Integer[] {3, 75}); validatePrefetch(gridView, 50, 0, new Integer[] {7, 75}); // no views returned for vertical prefetch: validatePrefetch(gridView, 0, 10); validatePrefetch(gridView, 0, -10); // test minor offset gridView.scrollBy(5, 0); validatePrefetch(gridView, -50, 0, new Integer[] {3, 80}); validatePrefetch(gridView, 50, 0, new Integer[] {7, 70}); } @Test public void prefetchRtl() { HorizontalGridView gridView = new HorizontalGridView(getContext()); gridView.setNumRows(1); gridView.setRowHeight(100); gridView.setAdapter(createBoxAdapter()); gridView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL); layout(gridView, 150, 100); // validate 2 children in viewport assertEquals(2, gridView.getChildCount()); assertEquals(50, gridView.getLayoutManager().findViewByPosition(0).getLeft()); assertEquals(-50, gridView.getLayoutManager().findViewByPosition(1).getLeft()); validatePrefetch(gridView, 50, 0); // no view to right validatePrefetch(gridView, -10, 0, new Integer[] {2, 50}); // next view 50 pixels to right // scroll to position 5, and layout gridView.scrollToPosition(5); layout(gridView, 150, 100); /* Visual representation, each number column represents 25 pixels: * | | * ... 7 7 6 6 6|6 5 5 5 5 4|4 4 4 3 3 ... * | | */ // validate 3 children in the viewport assertEquals(3, gridView.getChildCount()); assertNotNull(gridView.getLayoutManager().findViewByPosition(6)); assertNotNull(gridView.getLayoutManager().findViewByPosition(5)); assertNotNull(gridView.getLayoutManager().findViewByPosition(4)); assertEquals(-75, gridView.getLayoutManager().findViewByPosition(6).getLeft()); assertEquals(25, gridView.getLayoutManager().findViewByPosition(5).getLeft()); assertEquals(125, gridView.getLayoutManager().findViewByPosition(4).getLeft()); // next views are 75 pixels to right and left: validatePrefetch(gridView, 50, 0, new Integer[] {3, 75}); validatePrefetch(gridView, -50, 0, new Integer[] {7, 75}); // no views returned for vertical prefetch: validatePrefetch(gridView, 0, 10); validatePrefetch(gridView, 0, -10); // test minor offset gridView.scrollBy(-5, 0); validatePrefetch(gridView, 50, 0, new Integer[] {3, 80}); validatePrefetch(gridView, -50, 0, new Integer[] {7, 70}); } class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> { OuterAdapter() { for (int i = 0; i < getItemCount(); i++) { mAdapters.add(createBoxAdapter()); mSavedStates.add(null); } } class ViewHolder extends RecyclerView.ViewHolder { private final RecyclerView mRecyclerView; ViewHolder(RecyclerView itemView) { super(itemView); mRecyclerView = itemView; } } ArrayList<RecyclerView.Adapter> mAdapters = new ArrayList<>(); ArrayList<Parcelable> mSavedStates = new ArrayList<>(); RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool(); @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { HorizontalGridView gridView = new HorizontalGridView(getContext()); gridView.setNumRows(1); gridView.setRowHeight(100); gridView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); gridView.setLayoutParams(new GridLayoutManager.LayoutParams(350, 100)); gridView.setRecycledViewPool(mSharedPool); return new ViewHolder(gridView); } @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.mRecyclerView.swapAdapter(mAdapters.get(position), true); Parcelable savedState = mSavedStates.get(position); if (savedState != null) { holder.mRecyclerView.getLayoutManager().onRestoreInstanceState(savedState); mSavedStates.set(position, null); } } @Override public int getItemCount() { return 100; } }; public void validateInitialPrefetch(BaseGridView gridView, int... positionData) { RecyclerView.LayoutManager.LayoutPrefetchRegistry registry = mock(RecyclerView.LayoutManager.LayoutPrefetchRegistry.class); gridView.getLayoutManager().collectInitialPrefetchPositions( gridView.getAdapter().getItemCount(), registry); verify(registry, times(positionData.length)).addPosition(anyInt(), anyInt()); for (int position : positionData) { verify(registry).addPosition(position, 0); } } @Test public void prefetchInitialFocusTest() { VerticalGridView view = new VerticalGridView(getContext()); view.setNumColumns(1); view.setColumnWidth(350); view.setAdapter(createBoxAdapter()); // check default assertEquals(4, view.getInitialItemPrefetchCount()); // check setter behavior view.setInitialPrefetchItemCount(0); assertEquals(0, view.getInitialItemPrefetchCount()); // check positions fetched, relative to focus view.scrollToPosition(2); view.setInitialPrefetchItemCount(5); validateInitialPrefetch(view, 0, 1, 2, 3, 4); view.setInitialPrefetchItemCount(3); validateInitialPrefetch(view, 1, 2, 3); view.scrollToPosition(0); view.setInitialPrefetchItemCount(4); validateInitialPrefetch(view, 0, 1, 2, 3); view.scrollToPosition(98); view.setInitialPrefetchItemCount(5); validateInitialPrefetch(view, 95, 96, 97, 98, 99); view.setInitialPrefetchItemCount(7); validateInitialPrefetch(view, 93, 94, 95, 96, 97, 98, 99); // implementation detail - rounds up view.scrollToPosition(50); view.setInitialPrefetchItemCount(4); validateInitialPrefetch(view, 49, 50, 51, 52); } @Test public void prefetchNested() { VerticalGridView gridView = new VerticalGridView(getContext()); gridView.setNumColumns(1); gridView.setColumnWidth(350); OuterAdapter outerAdapter = new OuterAdapter(); gridView.setAdapter(outerAdapter); gridView.setItemViewCacheSize(1); // enough to cache child 0 while offscreen layout(gridView, 350, 150); // validate 2 top level children in viewport assertEquals(2, gridView.getChildCount()); for (int y = 0; y < 2; y++) { View child = gridView.getLayoutManager().findViewByPosition(y); assertEquals(y * 100, child.getTop()); // each has 4 children HorizontalGridView inner = (HorizontalGridView) child; for (int x = 0; x < 4; x++) { assertEquals(x * 100, inner.getLayoutManager().findViewByPosition(x).getLeft()); } } // center child 0 at position 10 HorizontalGridView offsetChild = (HorizontalGridView) gridView.getLayoutManager().findViewByPosition(0); offsetChild.scrollToPosition(10); // scroll to position 2, and layout gridView.scrollToPosition(2); layout(gridView, 350, 150); // now, offset by 175, centered around row 2. Validate 3 top level children in viewport assertEquals(3, gridView.getChildCount()); for (int y = 1; y < 4; y++) { assertEquals(y * 100 - 175, gridView.getLayoutManager().findViewByPosition(y).getTop()); } validatePrefetch(gridView, 0, -5, new Integer[] {0, 75}); validatePrefetch(gridView, 0, 5, new Integer[] {4, 75}); // assume offsetChild still bound, in cache, just not attached... validateInitialPrefetch(offsetChild, 9, 10, 11, 12); } }