Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package androidx.recyclerview.widget;
     17 
     18 import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
     19 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
     20 
     21 import static org.junit.Assert.assertEquals;
     22 
     23 import static java.util.concurrent.TimeUnit.SECONDS;
     24 
     25 import android.content.Context;
     26 import android.graphics.Rect;
     27 import android.view.View;
     28 
     29 import org.hamcrest.CoreMatchers;
     30 import org.hamcrest.MatcherAssert;
     31 
     32 import java.lang.reflect.Field;
     33 import java.util.ArrayList;
     34 import java.util.HashSet;
     35 import java.util.List;
     36 import java.util.Set;
     37 import java.util.concurrent.CountDownLatch;
     38 
     39 public class BaseGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
     40 
     41     static final String TAG = "GridLayoutManagerTest";
     42     static final boolean DEBUG = false;
     43 
     44     WrappedGridLayoutManager mGlm;
     45     GridTestAdapter mAdapter;
     46 
     47     public RecyclerView setupBasic(Config config) throws Throwable {
     48         return setupBasic(config, new GridTestAdapter(config.mItemCount));
     49     }
     50 
     51     public RecyclerView setupBasic(Config config, GridTestAdapter testAdapter) throws Throwable {
     52         RecyclerView recyclerView = new WrappedRecyclerView(getActivity());
     53         mAdapter = testAdapter;
     54         mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation,
     55                 config.mReverseLayout);
     56         mAdapter.assignSpanSizeLookup(mGlm);
     57         recyclerView.setAdapter(mAdapter);
     58         recyclerView.setLayoutManager(mGlm);
     59         return recyclerView;
     60     }
     61 
     62     public static List<Config> createBaseVariations() {
     63         List<Config> variations = new ArrayList<>();
     64         for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
     65             for (boolean reverseLayout : new boolean[]{false, true}) {
     66                 for (int spanCount : new int[]{1, 3, 4}) {
     67                     variations.add(new Config(spanCount, orientation, reverseLayout));
     68                 }
     69             }
     70         }
     71         return variations;
     72     }
     73 
     74     protected static List<Config> addConfigVariation(List<Config> base, String fieldName,
     75             Object... variations)
     76             throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException {
     77         List<Config> newConfigs = new ArrayList<Config>();
     78         Field field = Config.class.getDeclaredField(fieldName);
     79         for (Config config : base) {
     80             for (Object variation : variations) {
     81                 Config newConfig = (Config) config.clone();
     82                 field.set(newConfig, variation);
     83                 newConfigs.add(newConfig);
     84             }
     85         }
     86         return newConfigs;
     87     }
     88 
     89     public void waitForFirstLayout(RecyclerView recyclerView) throws Throwable {
     90         mGlm.expectLayout(1);
     91         setRecyclerView(recyclerView);
     92         mGlm.waitForLayout(2);
     93     }
     94 
     95     protected int getSize(View view) {
     96         if (mGlm.getOrientation() == GridLayoutManager.HORIZONTAL) {
     97             return view.getWidth();
     98         }
     99         return view.getHeight();
    100     }
    101 
    102     GridLayoutManager.LayoutParams getLp(View view) {
    103         return (GridLayoutManager.LayoutParams) view.getLayoutParams();
    104     }
    105 
    106     static class Config implements Cloneable {
    107 
    108         int mSpanCount;
    109         int mOrientation = GridLayoutManager.VERTICAL;
    110         int mItemCount = 1000;
    111         int mSpanPerItem = 1;
    112         boolean mReverseLayout = false;
    113 
    114         Config(int spanCount, int itemCount) {
    115             mSpanCount = spanCount;
    116             mItemCount = itemCount;
    117         }
    118 
    119         public Config(int spanCount, int orientation, boolean reverseLayout) {
    120             mSpanCount = spanCount;
    121             mOrientation = orientation;
    122             mReverseLayout = reverseLayout;
    123         }
    124 
    125         Config orientation(int orientation) {
    126             mOrientation = orientation;
    127             return this;
    128         }
    129 
    130         @Override
    131         public String toString() {
    132             return "Config{"
    133                     + "mSpanCount=" + mSpanCount
    134                     + ",mOrientation=" + (mOrientation == GridLayoutManager.HORIZONTAL ? "h" : "v")
    135                     + ",mItemCount=" + mItemCount
    136                     + ",mReverseLayout=" + mReverseLayout
    137                     + '}';
    138         }
    139 
    140         public Config reverseLayout(boolean reverseLayout) {
    141             mReverseLayout = reverseLayout;
    142             return this;
    143         }
    144 
    145         @Override
    146         protected Object clone() throws CloneNotSupportedException {
    147             return super.clone();
    148         }
    149     }
    150 
    151     class WrappedGridLayoutManager extends GridLayoutManager {
    152 
    153         CountDownLatch mLayoutLatch;
    154 
    155         CountDownLatch prefetchLatch;
    156 
    157         OrientationHelper mSecondaryOrientation;
    158 
    159         List<GridLayoutManagerTest.Callback>
    160                 mCallbacks = new ArrayList<GridLayoutManagerTest.Callback>();
    161 
    162         Boolean mFakeRTL;
    163         private CountDownLatch snapLatch;
    164 
    165         public WrappedGridLayoutManager(Context context, int spanCount) {
    166             super(context, spanCount);
    167         }
    168 
    169         public WrappedGridLayoutManager(Context context, int spanCount, int orientation,
    170                 boolean reverseLayout) {
    171             super(context, spanCount, orientation, reverseLayout);
    172         }
    173 
    174         @Override
    175         protected boolean isLayoutRTL() {
    176             return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL;
    177         }
    178 
    179         public void setFakeRtl(Boolean fakeRtl) {
    180             mFakeRTL = fakeRtl;
    181             try {
    182                 requestLayoutOnUIThread(mRecyclerView);
    183             } catch (Throwable throwable) {
    184                 postExceptionToInstrumentation(throwable);
    185             }
    186         }
    187 
    188         @Override
    189         public void setOrientation(int orientation) {
    190             super.setOrientation(orientation);
    191             mSecondaryOrientation = null;
    192         }
    193 
    194         @Override
    195         void ensureLayoutState() {
    196             super.ensureLayoutState();
    197             if (mSecondaryOrientation == null) {
    198                 if (getOrientation() == RecyclerView.HORIZONTAL) {
    199                     mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
    200                         RecyclerView.VERTICAL);
    201                 } else {
    202                     mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
    203                         RecyclerView.HORIZONTAL);
    204                 }
    205             }
    206         }
    207 
    208         @Override
    209         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    210             try {
    211                 for (GridLayoutManagerTest.Callback callback : mCallbacks) {
    212                     callback.onBeforeLayout(recycler, state);
    213                 }
    214                 super.onLayoutChildren(recycler, state);
    215                 for (GridLayoutManagerTest.Callback callback : mCallbacks) {
    216                     callback.onAfterLayout(recycler, state);
    217                 }
    218             } catch (Throwable t) {
    219                 postExceptionToInstrumentation(t);
    220             }
    221             mLayoutLatch.countDown();
    222         }
    223 
    224         @Override
    225         LayoutState createLayoutState() {
    226             return new LayoutState() {
    227                 @Override
    228                 View next(RecyclerView.Recycler recycler) {
    229                     final boolean hadMore = hasMore(mRecyclerView.mState);
    230                     final int position = mCurrentPosition;
    231                     View next = super.next(recycler);
    232                     assertEquals("if has more, should return a view", hadMore, next != null);
    233                     assertEquals("position of the returned view must match current position",
    234                             position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition());
    235                     return next;
    236                 }
    237             };
    238         }
    239 
    240         Rect getViewBounds(View view) {
    241             if (getOrientation() == HORIZONTAL) {
    242                 return new Rect(
    243                     mOrientationHelper.getDecoratedStart(view),
    244                     mSecondaryOrientation.getDecoratedStart(view),
    245                     mOrientationHelper.getDecoratedEnd(view),
    246                     mSecondaryOrientation.getDecoratedEnd(view));
    247             } else {
    248                 return new Rect(
    249                     mSecondaryOrientation.getDecoratedStart(view),
    250                     mOrientationHelper.getDecoratedStart(view),
    251                     mSecondaryOrientation.getDecoratedEnd(view),
    252                     mOrientationHelper.getDecoratedEnd(view));
    253             }
    254 
    255         }
    256 
    257         public void expectLayout(int layoutCount) {
    258             mLayoutLatch = new CountDownLatch(layoutCount);
    259         }
    260 
    261         public void waitForLayout(int seconds) throws Throwable {
    262             mLayoutLatch.await(seconds * (DEBUG ? 1000 : 1), SECONDS);
    263             checkForMainThreadException();
    264             MatcherAssert.assertThat("all layouts should complete on time",
    265                     mLayoutLatch.getCount(), CoreMatchers.is(0L));
    266             // use a runnable to ensure RV layout is finished
    267             getInstrumentation().runOnMainSync(new Runnable() {
    268                 @Override
    269                 public void run() {
    270                 }
    271             });
    272         }
    273 
    274         public void expectPrefetch(int count) {
    275             prefetchLatch = new CountDownLatch(count);
    276         }
    277 
    278         public void waitForPrefetch(int seconds) throws Throwable {
    279             prefetchLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
    280             checkForMainThreadException();
    281             MatcherAssert.assertThat("all prefetches should complete on time",
    282                     prefetchLatch.getCount(), CoreMatchers.is(0L));
    283             // use a runnable to ensure RV layout is finished
    284             getInstrumentation().runOnMainSync(new Runnable() {
    285                 @Override
    286                 public void run() {
    287                 }
    288             });
    289         }
    290 
    291         public void expectIdleState(int count) {
    292             snapLatch = new CountDownLatch(count);
    293             mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    294                 @Override
    295                 public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
    296                     super.onScrollStateChanged(recyclerView, newState);
    297                     if (newState == RecyclerView.SCROLL_STATE_IDLE) {
    298                         snapLatch.countDown();
    299                         if (snapLatch.getCount() == 0L) {
    300                             mRecyclerView.removeOnScrollListener(this);
    301                         }
    302                     }
    303                 }
    304             });
    305         }
    306 
    307         public void waitForSnap(int seconds) throws Throwable {
    308             snapLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
    309             checkForMainThreadException();
    310             MatcherAssert.assertThat("all scrolling should complete on time",
    311                     snapLatch.getCount(), CoreMatchers.is(0L));
    312             // use a runnable to ensure RV layout is finished
    313             getInstrumentation().runOnMainSync(new Runnable() {
    314                 @Override
    315                 public void run() {
    316                 }
    317             });
    318         }
    319 
    320         @Override
    321         public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
    322                 LayoutPrefetchRegistry layoutPrefetchRegistry) {
    323             if (prefetchLatch != null) prefetchLatch.countDown();
    324             super.collectAdjacentPrefetchPositions(dx, dy, state, layoutPrefetchRegistry);
    325         }
    326     }
    327 
    328     class GridFocusableAdapter extends FocusableAdapter {
    329 
    330         Set<Integer> mFullSpanItems = new HashSet<Integer>();
    331         int mSpanPerItem = 1;
    332 
    333         GridFocusableAdapter(int count) {
    334             this(count, 1);
    335         }
    336 
    337         GridFocusableAdapter(int count, int spanPerItem) {
    338             super(count);
    339             mSpanPerItem = spanPerItem;
    340         }
    341 
    342         void setFullSpan(int... items) {
    343             for (int i : items) {
    344                 mFullSpanItems.add(i);
    345             }
    346         }
    347 
    348         void assignSpanSizeLookup(final GridLayoutManager glm) {
    349             glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    350                 @Override
    351                 public int getSpanSize(int position) {
    352                     return mFullSpanItems.contains(position) ? glm.getSpanCount() : mSpanPerItem;
    353                 }
    354             });
    355         }
    356     }
    357 
    358     class GridTestAdapter extends TestAdapter {
    359 
    360         Set<Integer> mFullSpanItems = new HashSet<Integer>();
    361         int mSpanPerItem = 1;
    362 
    363         GridTestAdapter(int count) {
    364             super(count);
    365         }
    366 
    367         GridTestAdapter(int count, int spanPerItem) {
    368             super(count);
    369             mSpanPerItem = spanPerItem;
    370         }
    371 
    372         void setFullSpan(int... items) {
    373             for (int i : items) {
    374                 mFullSpanItems.add(i);
    375             }
    376         }
    377 
    378         void assignSpanSizeLookup(final GridLayoutManager glm) {
    379             glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    380                 @Override
    381                 public int getSpanSize(int position) {
    382                     return mFullSpanItems.contains(position) ? glm.getSpanCount() : mSpanPerItem;
    383                 }
    384             });
    385         }
    386     }
    387 
    388     class Callback {
    389 
    390         public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
    391         }
    392 
    393         public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
    394         }
    395     }
    396 }
    397