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