1 /* 2 * Copyright (C) 2016 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 android.support.v7.widget; 17 18 import android.content.Context; 19 import android.view.View; 20 21 import java.lang.reflect.Field; 22 import java.util.ArrayList; 23 import java.util.HashSet; 24 import java.util.List; 25 import java.util.Set; 26 import java.util.concurrent.CountDownLatch; 27 28 import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL; 29 import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 30 import static java.util.concurrent.TimeUnit.SECONDS; 31 import static org.junit.Assert.assertEquals; 32 33 import org.hamcrest.CoreMatchers; 34 import org.hamcrest.MatcherAssert; 35 36 public class BaseGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest { 37 38 static final String TAG = "GridLayoutManagerTest"; 39 static final boolean DEBUG = false; 40 41 WrappedGridLayoutManager mGlm; 42 GridTestAdapter mAdapter; 43 setupBasic(Config config)44 public RecyclerView setupBasic(Config config) throws Throwable { 45 return setupBasic(config, new GridTestAdapter(config.mItemCount)); 46 } 47 setupBasic(Config config, GridTestAdapter testAdapter)48 public RecyclerView setupBasic(Config config, GridTestAdapter testAdapter) throws Throwable { 49 RecyclerView recyclerView = new RecyclerView(getActivity()); 50 mAdapter = testAdapter; 51 mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation, 52 config.mReverseLayout); 53 mAdapter.assignSpanSizeLookup(mGlm); 54 recyclerView.setAdapter(mAdapter); 55 recyclerView.setLayoutManager(mGlm); 56 return recyclerView; 57 } 58 createBaseVariations()59 public static List<Config> createBaseVariations() { 60 List<Config> variations = new ArrayList<>(); 61 for (int orientation : new int[]{VERTICAL, HORIZONTAL}) { 62 for (boolean reverseLayout : new boolean[]{false, true}) { 63 for (int spanCount : new int[]{1, 3, 4}) { 64 variations.add(new Config(spanCount, orientation, reverseLayout)); 65 } 66 } 67 } 68 return variations; 69 } 70 addConfigVariation(List<Config> base, String fieldName, Object... variations)71 protected static List<Config> addConfigVariation(List<Config> base, String fieldName, 72 Object... variations) 73 throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException { 74 List<Config> newConfigs = new ArrayList<Config>(); 75 Field field = Config.class.getDeclaredField(fieldName); 76 for (Config config : base) { 77 for (Object variation : variations) { 78 Config newConfig = (Config) config.clone(); 79 field.set(newConfig, variation); 80 newConfigs.add(newConfig); 81 } 82 } 83 return newConfigs; 84 } 85 waitForFirstLayout(RecyclerView recyclerView)86 public void waitForFirstLayout(RecyclerView recyclerView) throws Throwable { 87 mGlm.expectLayout(1); 88 setRecyclerView(recyclerView); 89 mGlm.waitForLayout(2); 90 } 91 getSize(View view)92 protected int getSize(View view) { 93 if (mGlm.getOrientation() == GridLayoutManager.HORIZONTAL) { 94 return view.getWidth(); 95 } 96 return view.getHeight(); 97 } 98 getLp(View view)99 GridLayoutManager.LayoutParams getLp(View view) { 100 return (GridLayoutManager.LayoutParams) view.getLayoutParams(); 101 } 102 103 static class Config implements Cloneable { 104 105 int mSpanCount; 106 int mOrientation = GridLayoutManager.VERTICAL; 107 int mItemCount = 1000; 108 int mSpanPerItem = 1; 109 boolean mReverseLayout = false; 110 Config(int spanCount, int itemCount)111 Config(int spanCount, int itemCount) { 112 mSpanCount = spanCount; 113 mItemCount = itemCount; 114 } 115 Config(int spanCount, int orientation, boolean reverseLayout)116 public Config(int spanCount, int orientation, boolean reverseLayout) { 117 mSpanCount = spanCount; 118 mOrientation = orientation; 119 mReverseLayout = reverseLayout; 120 } 121 orientation(int orientation)122 Config orientation(int orientation) { 123 mOrientation = orientation; 124 return this; 125 } 126 127 @Override toString()128 public String toString() { 129 return "Config{" + 130 "mSpanCount=" + mSpanCount + 131 ", mOrientation=" + (mOrientation == GridLayoutManager.HORIZONTAL ? "h" : "v") + 132 ", mItemCount=" + mItemCount + 133 ", mReverseLayout=" + mReverseLayout + 134 '}'; 135 } 136 reverseLayout(boolean reverseLayout)137 public Config reverseLayout(boolean reverseLayout) { 138 mReverseLayout = reverseLayout; 139 return this; 140 } 141 142 @Override clone()143 protected Object clone() throws CloneNotSupportedException { 144 return super.clone(); 145 } 146 } 147 148 class WrappedGridLayoutManager extends GridLayoutManager { 149 150 CountDownLatch mLayoutLatch; 151 152 List<GridLayoutManagerTest.Callback> 153 mCallbacks = new ArrayList<GridLayoutManagerTest.Callback>(); 154 155 Boolean mFakeRTL; 156 private CountDownLatch snapLatch; 157 WrappedGridLayoutManager(Context context, int spanCount)158 public WrappedGridLayoutManager(Context context, int spanCount) { 159 super(context, spanCount); 160 } 161 WrappedGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout)162 public WrappedGridLayoutManager(Context context, int spanCount, int orientation, 163 boolean reverseLayout) { 164 super(context, spanCount, orientation, reverseLayout); 165 } 166 167 @Override isLayoutRTL()168 protected boolean isLayoutRTL() { 169 return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL; 170 } 171 setFakeRtl(Boolean fakeRtl)172 public void setFakeRtl(Boolean fakeRtl) { 173 mFakeRTL = fakeRtl; 174 try { 175 requestLayoutOnUIThread(mRecyclerView); 176 } catch (Throwable throwable) { 177 postExceptionToInstrumentation(throwable); 178 } 179 } 180 181 @Override onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)182 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 183 try { 184 for (GridLayoutManagerTest.Callback callback : mCallbacks) { 185 callback.onBeforeLayout(recycler, state); 186 } 187 super.onLayoutChildren(recycler, state); 188 for (GridLayoutManagerTest.Callback callback : mCallbacks) { 189 callback.onAfterLayout(recycler, state); 190 } 191 } catch (Throwable t) { 192 postExceptionToInstrumentation(t); 193 } 194 mLayoutLatch.countDown(); 195 } 196 197 @Override createLayoutState()198 LayoutState createLayoutState() { 199 return new LayoutState() { 200 @Override 201 View next(RecyclerView.Recycler recycler) { 202 final boolean hadMore = hasMore(mRecyclerView.mState); 203 final int position = mCurrentPosition; 204 View next = super.next(recycler); 205 assertEquals("if has more, should return a view", hadMore, next != null); 206 assertEquals("position of the returned view must match current position", 207 position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition()); 208 return next; 209 } 210 }; 211 } 212 expectLayout(int layoutCount)213 public void expectLayout(int layoutCount) { 214 mLayoutLatch = new CountDownLatch(layoutCount); 215 } 216 waitForLayout(int seconds)217 public void waitForLayout(int seconds) throws Throwable { 218 mLayoutLatch.await(seconds * (DEBUG ? 1000 : 1), SECONDS); 219 checkForMainThreadException(); 220 MatcherAssert.assertThat("all layouts should complete on time", 221 mLayoutLatch.getCount(), CoreMatchers.is(0L)); 222 // use a runnable to ensure RV layout is finished 223 getInstrumentation().runOnMainSync(new Runnable() { 224 @Override 225 public void run() { 226 } 227 }); 228 } 229 expectIdleState(int count)230 public void expectIdleState(int count) { 231 snapLatch = new CountDownLatch(count); 232 mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 233 @Override 234 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 235 super.onScrollStateChanged(recyclerView, newState); 236 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 237 snapLatch.countDown(); 238 if (snapLatch.getCount() == 0L) { 239 mRecyclerView.removeOnScrollListener(this); 240 } 241 } 242 } 243 }); 244 } 245 waitForSnap(int seconds)246 public void waitForSnap(int seconds) throws Throwable { 247 snapLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS); 248 checkForMainThreadException(); 249 MatcherAssert.assertThat("all scrolling should complete on time", 250 snapLatch.getCount(), CoreMatchers.is(0L)); 251 // use a runnable to ensure RV layout is finished 252 getInstrumentation().runOnMainSync(new Runnable() { 253 @Override 254 public void run() { 255 } 256 }); 257 } 258 } 259 260 class GridTestAdapter extends TestAdapter { 261 262 Set<Integer> mFullSpanItems = new HashSet<Integer>(); 263 int mSpanPerItem = 1; 264 265 GridTestAdapter(int count) { 266 super(count); 267 } 268 269 GridTestAdapter(int count, int spanPerItem) { 270 super(count); 271 mSpanPerItem = spanPerItem; 272 } 273 274 void setFullSpan(int... items) { 275 for (int i : items) { 276 mFullSpanItems.add(i); 277 } 278 } 279 280 void assignSpanSizeLookup(final GridLayoutManager glm) { 281 glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { 282 @Override 283 public int getSpanSize(int position) { 284 return mFullSpanItems.contains(position) ? glm.getSpanCount() : mSpanPerItem; 285 } 286 }); 287 } 288 } 289 290 class Callback { 291 292 public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) { 293 } 294 295 public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) { 296 } 297 } 298 } 299