1/*
2 * Copyright (C) 2015 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 static org.hamcrest.CoreMatchers.is;
19import static org.hamcrest.MatcherAssert.assertThat;
20import static org.junit.Assert.assertEquals;
21import static org.junit.Assert.assertNull;
22import static org.junit.Assert.assertTrue;
23
24import android.app.Activity;
25import android.graphics.Color;
26import android.graphics.Rect;
27import android.support.annotation.Nullable;
28import android.support.v4.util.LongSparseArray;
29import android.support.v7.widget.TestedFrameLayout.FullControlLayoutParams;
30import android.util.Log;
31import android.view.Gravity;
32import android.view.View;
33import android.view.ViewGroup;
34import android.widget.TextView;
35
36import org.hamcrest.CoreMatchers;
37
38import java.util.ArrayList;
39import java.util.Collections;
40import java.util.List;
41
42/**
43 * Class to test any generic wrap content behavior.
44 * It does so by running the same view scenario twice. Once with match parent setup to record all
45 * dimensions and once with wrap_content setup. Then compares all child locations & ids +
46 * RecyclerView size.
47 */
48abstract public class BaseWrapContentTest extends BaseRecyclerViewInstrumentationTest {
49
50    static final boolean DEBUG = false;
51    static final String TAG = "WrapContentTest";
52    RecyclerView.LayoutManager mLayoutManager;
53
54    TestAdapter mTestAdapter;
55
56    LoggingItemAnimator mLoggingItemAnimator;
57
58    boolean mIsWrapContent;
59
60    protected final WrapContentConfig mWrapContentConfig;
61
62    public BaseWrapContentTest(WrapContentConfig config) {
63        mWrapContentConfig = config;
64    }
65
66    abstract RecyclerView.LayoutManager createLayoutManager();
67
68    void unspecifiedWithHintTest(boolean horizontal) throws Throwable {
69        final int itemHeight = 20;
70        final int itemWidth = 15;
71        RecyclerView.LayoutManager layoutManager = createLayoutManager();
72        WrappedRecyclerView rv = createRecyclerView(getActivity());
73        TestAdapter testAdapter = new TestAdapter(20) {
74            @Override
75            public void onBindViewHolder(TestViewHolder holder,
76                    int position) {
77                super.onBindViewHolder(holder, position);
78                holder.itemView.setLayoutParams(new ViewGroup.LayoutParams(itemWidth, itemHeight));
79            }
80        };
81        rv.setLayoutManager(layoutManager);
82        rv.setAdapter(testAdapter);
83        TestedFrameLayout.FullControlLayoutParams lp =
84                new TestedFrameLayout.FullControlLayoutParams(0, 0);
85        if (horizontal) {
86            lp.wSpec = View.MeasureSpec.makeMeasureSpec(25, View.MeasureSpec.UNSPECIFIED);
87            lp.hSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.AT_MOST);
88        } else {
89            lp.hSpec = View.MeasureSpec.makeMeasureSpec(25, View.MeasureSpec.UNSPECIFIED);
90            lp.wSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.AT_MOST);
91        }
92        rv.setLayoutParams(lp);
93        setRecyclerView(rv);
94        rv.waitUntilLayout();
95
96        // we don't assert against the given size hint because LM will still ask for more if it
97        // lays out more children. This is the correct behavior because the spec is not AT_MOST,
98        // it is UNSPECIFIED.
99        if (horizontal) {
100            int expectedWidth = rv.getPaddingLeft() + rv.getPaddingRight() + itemWidth;
101            while (expectedWidth < 25) {
102                expectedWidth += itemWidth;
103            }
104            assertThat(rv.getWidth(), CoreMatchers.is(expectedWidth));
105        } else {
106            int expectedHeight = rv.getPaddingTop() + rv.getPaddingBottom() + itemHeight;
107            while (expectedHeight < 25) {
108                expectedHeight += itemHeight;
109            }
110            assertThat(rv.getHeight(), CoreMatchers.is(expectedHeight));
111        }
112    }
113
114    protected void testScenerio(Scenario scenario) throws Throwable {
115        FullControlLayoutParams matchParent = new FullControlLayoutParams(
116                ViewGroup.LayoutParams.MATCH_PARENT,
117                ViewGroup.LayoutParams.MATCH_PARENT);
118        FullControlLayoutParams wrapContent = new FullControlLayoutParams(
119                ViewGroup.LayoutParams.WRAP_CONTENT,
120                ViewGroup.LayoutParams.WRAP_CONTENT);
121        if (mWrapContentConfig.isUnlimitedHeight()) {
122            wrapContent.hSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
123        }
124        if (mWrapContentConfig.isUnlimitedWidth()) {
125            wrapContent.wSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
126        }
127
128        mIsWrapContent = false;
129        List<Snapshot> s1 = runScenario(scenario, matchParent, null);
130        mIsWrapContent = true;
131
132        List<Snapshot> s2 = runScenario(scenario, wrapContent, s1);
133        assertEquals("test sanity", s1.size(), s2.size());
134
135        for (int i = 0; i < s1.size(); i++) {
136            Snapshot step1 = s1.get(i);
137            Snapshot step2 = s2.get(i);
138            step1.assertSame(step2, i);
139        }
140    }
141
142    public List<Snapshot> runScenario(Scenario scenario, ViewGroup.LayoutParams lp,
143            @Nullable List<Snapshot> compareWith)
144            throws Throwable {
145        removeRecyclerView();
146        Item.idCounter.set(0);
147        List<Snapshot> result = new ArrayList<>();
148        RecyclerView.LayoutManager layoutManager = scenario.createLayoutManager();
149        WrappedRecyclerView recyclerView = new WrappedRecyclerView(getActivity());
150        recyclerView.setBackgroundColor(Color.rgb(0, 0, 255));
151        recyclerView.setLayoutManager(layoutManager);
152        recyclerView.setLayoutParams(lp);
153        mLayoutManager = layoutManager;
154        mTestAdapter = new TestAdapter(scenario.getSeedAdapterSize());
155        recyclerView.setAdapter(mTestAdapter);
156        mLoggingItemAnimator = new LoggingItemAnimator();
157        recyclerView.setItemAnimator(mLoggingItemAnimator);
158        setRecyclerView(recyclerView);
159        recyclerView.waitUntilLayout();
160        int stepIndex = 0;
161        for (Step step : scenario.mStepList) {
162            mLoggingItemAnimator.reset();
163            step.onRun();
164            recyclerView.waitUntilLayout();
165            recyclerView.waitUntilAnimations();
166            Snapshot snapshot = takeSnapshot();
167            if (mIsWrapContent) {
168                snapshot.assertRvSize();
169            }
170            result.add(snapshot);
171            if (compareWith != null) {
172                compareWith.get(stepIndex).assertSame(snapshot, stepIndex);
173            }
174            stepIndex++;
175        }
176        recyclerView.waitUntilLayout();
177        recyclerView.waitUntilAnimations();
178        Snapshot snapshot = takeSnapshot();
179        if (mIsWrapContent) {
180            snapshot.assertRvSize();
181        }
182        result.add(snapshot);
183        if (compareWith != null) {
184            compareWith.get(stepIndex).assertSame(snapshot, stepIndex);
185        }
186        return result;
187    }
188
189    protected WrappedRecyclerView createRecyclerView(Activity activity) {
190        return new WrappedRecyclerView(getActivity());
191    }
192
193    void layoutAndCheck(TestedFrameLayout.FullControlLayoutParams lp,
194            BaseWrapContentWithAspectRatioTest.WrapContentAdapter adapter, Rect[] expected,
195            int width, int height) throws Throwable {
196        WrappedRecyclerView recyclerView = createRecyclerView(getActivity());
197        recyclerView.setBackgroundColor(Color.rgb(0, 0, 255));
198        recyclerView.setLayoutManager(createLayoutManager());
199        recyclerView.setAdapter(adapter);
200        recyclerView.setLayoutParams(lp);
201        Rect padding = mWrapContentConfig.padding;
202        recyclerView.setPadding(padding.left, padding.top, padding.right, padding.bottom);
203        setRecyclerView(recyclerView);
204        recyclerView.waitUntilLayout();
205        Snapshot snapshot = takeSnapshot();
206        int index = 0;
207        Rect tmp = new Rect();
208        for (BaseWrapContentWithAspectRatioTest.MeasureBehavior behavior : adapter.behaviors) {
209            tmp.set(expected[index]);
210            tmp.offset(padding.left, padding.top);
211            assertThat("behavior " + index, snapshot.mChildCoordinates.get(behavior.getId()),
212                    is(tmp));
213            index ++;
214        }
215        Rect boundingBox = new Rect(0, 0, 0, 0);
216        for (Rect rect : expected) {
217            boundingBox.union(rect);
218        }
219        assertThat(recyclerView.getWidth(), is(width + padding.left + padding.right));
220        assertThat(recyclerView.getHeight(), is(height + padding.top + padding.bottom));
221    }
222
223
224    abstract protected int getVerticalGravity(RecyclerView.LayoutManager layoutManager);
225
226    abstract protected int getHorizontalGravity(RecyclerView.LayoutManager layoutManager);
227
228    protected Snapshot takeSnapshot() throws Throwable {
229        Snapshot snapshot = new Snapshot(mRecyclerView, mLoggingItemAnimator,
230                getHorizontalGravity(mLayoutManager), getVerticalGravity(mLayoutManager));
231        return snapshot;
232    }
233
234    abstract class Scenario {
235
236        ArrayList<Step> mStepList = new ArrayList<>();
237
238        public Scenario(Step... steps) {
239            Collections.addAll(mStepList, steps);
240        }
241
242        public int getSeedAdapterSize() {
243            return 10;
244        }
245
246        public RecyclerView.LayoutManager createLayoutManager() {
247            return BaseWrapContentTest.this.createLayoutManager();
248        }
249    }
250
251    abstract static class Step {
252
253        abstract void onRun() throws Throwable;
254    }
255
256    class Snapshot {
257
258        Rect mRawChildrenBox = new Rect();
259
260        Rect mRvSize = new Rect();
261
262        Rect mRvPadding = new Rect();
263
264        Rect mRvParentSize = new Rect();
265
266        LongSparseArray<Rect> mChildCoordinates = new LongSparseArray<>();
267
268        LongSparseArray<String> mAppear = new LongSparseArray<>();
269
270        LongSparseArray<String> mDisappear = new LongSparseArray<>();
271
272        LongSparseArray<String> mPersistent = new LongSparseArray<>();
273
274        LongSparseArray<String> mChanged = new LongSparseArray<>();
275
276        int mVerticalGravity;
277
278        int mHorizontalGravity;
279
280        int mOffsetX, mOffsetY;// how much we should offset children
281
282        public Snapshot(RecyclerView recyclerView, LoggingItemAnimator loggingItemAnimator,
283                int horizontalGravity, int verticalGravity)
284                throws Throwable {
285            mRvSize = getViewBounds(recyclerView);
286            mRvParentSize = getViewBounds((View) recyclerView.getParent());
287            mRvPadding = new Rect(recyclerView.getPaddingLeft(), recyclerView.getPaddingTop(),
288                    recyclerView.getPaddingRight(), recyclerView.getPaddingBottom());
289            mVerticalGravity = verticalGravity;
290            mHorizontalGravity = horizontalGravity;
291            if (mVerticalGravity == Gravity.TOP) {
292                mOffsetY = 0;
293            } else {
294                mOffsetY = mRvParentSize.bottom - mRvSize.bottom;
295            }
296
297            if (mHorizontalGravity == Gravity.LEFT) {
298                mOffsetX = 0;
299            } else {
300                mOffsetX = mRvParentSize.right - mRvSize.right;
301            }
302            collectChildCoordinates(recyclerView);
303            if (loggingItemAnimator != null) {
304                collectInto(mAppear, loggingItemAnimator.mAnimateAppearanceList);
305                collectInto(mDisappear, loggingItemAnimator.mAnimateDisappearanceList);
306                collectInto(mPersistent, loggingItemAnimator.mAnimatePersistenceList);
307                collectInto(mChanged, loggingItemAnimator.mAnimateChangeList);
308            }
309        }
310
311        public boolean doesChildrenFitVertically() {
312            return mRawChildrenBox.top >= mRvPadding.top
313                    && mRawChildrenBox.bottom <= mRvSize.bottom - mRvPadding.bottom;
314        }
315
316        public boolean doesChildrenFitHorizontally() {
317            return mRawChildrenBox.left >= mRvPadding.left
318                    && mRawChildrenBox.right <= mRvSize.right - mRvPadding.right;
319        }
320
321        public void assertSame(Snapshot other, int step) {
322            if (mWrapContentConfig.isUnlimitedHeight() &&
323                    (!doesChildrenFitVertically() || !other.doesChildrenFitVertically())) {
324                if (DEBUG) {
325                    Log.d(TAG, "cannot assert coordinates because it does not fit vertically");
326                }
327                return;
328            }
329            if (mWrapContentConfig.isUnlimitedWidth() &&
330                    (!doesChildrenFitHorizontally() || !other.doesChildrenFitHorizontally())) {
331                if (DEBUG) {
332                    Log.d(TAG, "cannot assert coordinates because it does not fit horizontally");
333                }
334                return;
335            }
336            assertMap("child coordinates. step:" + step, mChildCoordinates,
337                    other.mChildCoordinates);
338            if (mWrapContentConfig.isUnlimitedHeight() || mWrapContentConfig.isUnlimitedWidth()) {
339                return;//cannot assert animatinos in unlimited size
340            }
341            assertMap("appearing step:" + step, mAppear, other.mAppear);
342            assertMap("disappearing step:" + step, mDisappear, other.mDisappear);
343            assertMap("persistent step:" + step, mPersistent, other.mPersistent);
344            assertMap("changed step:" + step, mChanged, other.mChanged);
345        }
346
347        private void assertMap(String prefix, LongSparseArray<?> map1, LongSparseArray<?> map2) {
348            StringBuilder logBuilder = new StringBuilder();
349            logBuilder.append(prefix).append("\n");
350            logBuilder.append("map1").append("\n");
351            logInto(map1, logBuilder);
352            logBuilder.append("map2").append("\n");
353            logInto(map2, logBuilder);
354            final String log = logBuilder.toString();
355            assertEquals(log + " same size", map1.size(), map2.size());
356            for (int i = 0; i < map1.size(); i++) {
357                assertAtIndex(log, map1, map2, i);
358            }
359        }
360
361        private void assertAtIndex(String prefix, LongSparseArray<?> map1, LongSparseArray<?> map2,
362                int index) {
363            long key1 = map1.keyAt(index);
364            long key2 = map2.keyAt(index);
365            assertEquals(prefix + "key mismatch at index " + index, key1, key2);
366            Object value1 = map1.valueAt(index);
367            Object value2 = map2.valueAt(index);
368            assertEquals(prefix + " value mismatch at index " + index, value1, value2);
369        }
370
371        private void logInto(LongSparseArray<?> map, StringBuilder sb) {
372            for (int i = 0; i < map.size(); i++) {
373                long key = map.keyAt(i);
374                Object value = map.valueAt(i);
375                sb.append(key).append(" : ").append(value).append("\n");
376            }
377        }
378
379        @Override
380        public String toString() {
381            StringBuilder sb = new StringBuilder("Snapshot{\n");
382            sb.append("child coordinates:\n");
383            logInto(mChildCoordinates, sb);
384            sb.append("appear animations:\n");
385            logInto(mAppear, sb);
386            sb.append("disappear animations:\n");
387            logInto(mDisappear, sb);
388            sb.append("change animations:\n");
389            logInto(mChanged, sb);
390            sb.append("persistent animations:\n");
391            logInto(mPersistent, sb);
392            sb.append("}");
393            return sb.toString();
394        }
395
396        @Override
397        public int hashCode() {
398            int result = mChildCoordinates.hashCode();
399            result = 31 * result + mAppear.hashCode();
400            result = 31 * result + mDisappear.hashCode();
401            result = 31 * result + mPersistent.hashCode();
402            result = 31 * result + mChanged.hashCode();
403            return result;
404        }
405
406        private void collectInto(
407                LongSparseArray<String> target,
408                List<? extends BaseRecyclerViewAnimationsTest.AnimateLogBase> list) {
409            for (BaseRecyclerViewAnimationsTest.AnimateLogBase base : list) {
410                long id = getItemId(base.viewHolder);
411                assertNull(target.get(id));
412                target.put(id, log(base));
413            }
414        }
415
416        private String log(BaseRecyclerViewAnimationsTest.AnimateLogBase base) {
417            return base.getClass().getSimpleName() +
418                    ((TextView) base.viewHolder.itemView).getText() + ": " +
419                    "[pre:" + log(base.postInfo) +
420                    ", post:" + log(base.postInfo) + "]";
421        }
422
423        private String log(BaseRecyclerViewAnimationsTest.LoggingInfo postInfo) {
424            if (postInfo == null) {
425                return "?";
426            }
427            return "PI[flags: " + postInfo.changeFlags
428                    + ",l:" + (postInfo.left + mOffsetX)
429                    + ",t:" + (postInfo.top + mOffsetY)
430                    + ",r:" + (postInfo.right + mOffsetX)
431                    + ",b:" + (postInfo.bottom + mOffsetY) + "]";
432        }
433
434        void collectChildCoordinates(RecyclerView recyclerView) throws Throwable {
435            mRawChildrenBox = new Rect(0, 0, 0, 0);
436            final int childCount = recyclerView.getChildCount();
437            for (int i = 0; i < childCount; i++) {
438                View child = recyclerView.getChildAt(i);
439                Rect childBounds = getChildBounds(recyclerView, child, true);
440                mRawChildrenBox.union(getChildBounds(recyclerView, child, false));
441                RecyclerView.ViewHolder childViewHolder = recyclerView.getChildViewHolder(child);
442                mChildCoordinates.put(getItemId(childViewHolder), childBounds);
443            }
444        }
445
446        private Rect getViewBounds(View view) {
447            return new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
448        }
449
450        private Rect getChildBounds(RecyclerView recyclerView, View child, boolean offset) {
451            RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
452            RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
453            Rect rect = new Rect(layoutManager.getDecoratedLeft(child) - lp.leftMargin,
454                    layoutManager.getDecoratedTop(child) - lp.topMargin,
455                    layoutManager.getDecoratedRight(child) + lp.rightMargin,
456                    layoutManager.getDecoratedBottom(child) + lp.bottomMargin);
457            if (offset) {
458                rect.offset(mOffsetX, mOffsetY);
459            }
460            return rect;
461        }
462
463        private long getItemId(RecyclerView.ViewHolder vh) {
464            if (vh instanceof TestViewHolder) {
465                return ((TestViewHolder) vh).mBoundItem.mId;
466            } else if (vh instanceof BaseWrapContentWithAspectRatioTest.WrapContentViewHolder) {
467                BaseWrapContentWithAspectRatioTest.WrapContentViewHolder casted =
468                        (BaseWrapContentWithAspectRatioTest.WrapContentViewHolder) vh;
469                return casted.mView.mBehavior.getId();
470            } else {
471                throw new IllegalArgumentException("i don't support any VH");
472            }
473        }
474
475        public void assertRvSize() {
476            if (shouldWrapContentHorizontally()) {
477                int expectedW = mRawChildrenBox.width() + mRvPadding.left + mRvPadding.right;
478                assertTrue(mRvSize.width() + " <= " + expectedW, mRvSize.width() <= expectedW);
479            }
480            if (shouldWrapContentVertically()) {
481                int expectedH = mRawChildrenBox.height() + mRvPadding.top + mRvPadding.bottom;
482                assertTrue(mRvSize.height() + "<=" + expectedH, mRvSize.height() <= expectedH);
483            }
484        }
485    }
486
487    protected boolean shouldWrapContentHorizontally() {
488        return true;
489    }
490
491    protected boolean shouldWrapContentVertically() {
492        return true;
493    }
494
495    static class WrapContentConfig {
496
497        public boolean unlimitedWidth;
498        public boolean unlimitedHeight;
499        public Rect padding = new Rect(0, 0, 0, 0);
500
501        public WrapContentConfig(boolean unlimitedWidth, boolean unlimitedHeight) {
502            this.unlimitedWidth = unlimitedWidth;
503            this.unlimitedHeight = unlimitedHeight;
504        }
505
506        public WrapContentConfig(boolean unlimitedWidth, boolean unlimitedHeight, Rect padding) {
507            this.unlimitedWidth = unlimitedWidth;
508            this.unlimitedHeight = unlimitedHeight;
509            this.padding.set(padding);
510        }
511
512        public boolean isUnlimitedWidth() {
513            return unlimitedWidth;
514        }
515
516        public WrapContentConfig setUnlimitedWidth(boolean unlimitedWidth) {
517            this.unlimitedWidth = unlimitedWidth;
518            return this;
519        }
520
521        public boolean isUnlimitedHeight() {
522            return unlimitedHeight;
523        }
524
525        public WrapContentConfig setUnlimitedHeight(boolean unlimitedHeight) {
526            this.unlimitedHeight = unlimitedHeight;
527            return this;
528        }
529
530        @Override
531        public String toString() {
532            return "WrapContentConfig{"
533                    + "unlimitedWidth=" + unlimitedWidth
534                    + ",unlimitedHeight=" + unlimitedHeight
535                    + ",padding=" + padding
536                    + '}';
537        }
538
539        public TestedFrameLayout.FullControlLayoutParams toLayoutParams(int wDim, int hDim) {
540            TestedFrameLayout.FullControlLayoutParams
541                    lp = new TestedFrameLayout.FullControlLayoutParams(
542                    wDim, hDim);
543            if (unlimitedWidth) {
544                lp.wSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
545            }
546            if (unlimitedHeight) {
547                lp.hSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
548            }
549            return lp;
550        }
551    }
552}
553