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