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