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
17package androidx.recyclerview.widget;
18
19import static android.view.View.MeasureSpec.AT_MOST;
20import static android.view.View.MeasureSpec.EXACTLY;
21import static android.view.View.MeasureSpec.makeMeasureSpec;
22
23import static org.hamcrest.CoreMatchers.is;
24import static org.hamcrest.CoreMatchers.notNullValue;
25import static org.hamcrest.MatcherAssert.assertThat;
26
27import android.graphics.Rect;
28import android.support.test.filters.MediumTest;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.FrameLayout;
32
33import androidx.annotation.NonNull;
34
35import org.junit.Test;
36import org.junit.runner.RunWith;
37import org.junit.runners.Parameterized;
38
39import java.util.ArrayList;
40import java.util.HashMap;
41import java.util.List;
42import java.util.Map;
43
44/**
45 * Tests whether the layout manager can keep its children positions properly after it is re-laid
46 * out with larger/smaller intermediate size but the same final size.
47 */
48@MediumTest
49@RunWith(Parameterized.class)
50public class TestResizingRelayoutWithAutoMeasure extends BaseRecyclerViewInstrumentationTest {
51    private final int mRvWidth;
52    private final int mRvHeight;
53    private final RecyclerView.LayoutManager mLayoutManager;
54    private final float mWidthMultiplier;
55    private final float mHeightMultiplier;
56
57    public TestResizingRelayoutWithAutoMeasure(@SuppressWarnings("UnusedParameters") String name,
58            int rvWidth, int rvHeight,
59            RecyclerView.LayoutManager layoutManager, float widthMultiplier,
60            float heightMultiplier) {
61        mRvWidth = rvWidth;
62        mRvHeight = rvHeight;
63        mLayoutManager = layoutManager;
64        mWidthMultiplier = widthMultiplier;
65        mHeightMultiplier = heightMultiplier;
66    }
67
68    @Parameterized.Parameters(name = "{0} rv w/h:{1}/{2} changed w/h:{4}/{5}")
69    public static List<Object[]> getParams() {
70        List<Object[]> params = new ArrayList<>();
71        for(int[] rvSize : new int[][]{new int[]{200, 200}, new int[]{200, 100},
72                new int[]{100, 200}}) {
73            for (float w : new float[]{.5f, 1f, 2f}) {
74                for (float h : new float[]{.5f, 1f, 2f}) {
75                    params.add(
76                            new Object[]{"linear layout", rvSize[0], rvSize[1],
77                                    new LinearLayoutManager(null), w, h}
78                    );
79                    params.add(
80                            new Object[]{"grid layout", rvSize[0], rvSize[1],
81                                    new GridLayoutManager(null, 3), w, h}
82                    );
83                    params.add(
84                            new Object[]{"staggered", rvSize[0], rvSize[1],
85                                    new StaggeredGridLayoutManager(3,
86                                    StaggeredGridLayoutManager.VERTICAL), w, h}
87                    );
88                }
89            }
90        }
91        return params;
92    }
93
94    @Test
95    public void testResizeDuringMeasurements() throws Throwable {
96        final WrappedRecyclerView recyclerView = new WrappedRecyclerView(getActivity());
97        recyclerView.setLayoutManager(mLayoutManager);
98        StaticAdapter adapter = new StaticAdapter(50, ViewGroup.LayoutParams.MATCH_PARENT,
99                mRvHeight / 5);
100        recyclerView.setLayoutParams(new FrameLayout.LayoutParams(mRvWidth, mRvHeight));
101        recyclerView.setAdapter(adapter);
102        setRecyclerView(recyclerView);
103        getInstrumentation().waitForIdleSync();
104        assertThat("Test sanity", recyclerView.getChildCount() > 0, is(true));
105        final int lastPosition = recyclerView.getAdapter().getItemCount() - 1;
106        smoothScrollToPosition(lastPosition);
107        assertThat("test sanity", recyclerView.findViewHolderForAdapterPosition(lastPosition),
108                notNullValue());
109        assertThat("test sanity", mRvWidth, is(recyclerView.getWidth()));
110        assertThat("test sanity", mRvHeight, is(recyclerView.getHeight()));
111        recyclerView.waitUntilLayout();
112        recyclerView.waitUntilAnimations();
113        final Map<Integer, Rect> startPositions = capturePositions(recyclerView);
114        mActivityRule.runOnUiThread(new Runnable() {
115            @Override
116            public void run() {
117                recyclerView.measure(
118                        makeMeasureSpec((int) (mRvWidth * mWidthMultiplier),
119                                mWidthMultiplier == 1f ? EXACTLY : AT_MOST),
120                        makeMeasureSpec((int) (mRvHeight * mHeightMultiplier),
121                                mHeightMultiplier == 1f ? EXACTLY : AT_MOST));
122
123                recyclerView.measure(
124                        makeMeasureSpec(mRvWidth, EXACTLY),
125                        makeMeasureSpec(mRvHeight, EXACTLY));
126                recyclerView.dispatchLayout();
127                Map<Integer, Rect> endPositions = capturePositions(recyclerView);
128                assertStartItemPositions(startPositions, endPositions);
129            }
130        });
131        recyclerView.waitUntilLayout();
132        recyclerView.waitUntilAnimations();
133        checkForMainThreadException();
134    }
135
136    private void assertStartItemPositions(Map<Integer, Rect> startPositions,
137            Map<Integer, Rect> endPositions) {
138        String log = log(startPositions, endPositions);
139        for (Map.Entry<Integer, Rect> entry : startPositions.entrySet()) {
140            Rect rect = endPositions.get(entry.getKey());
141            assertThat(log + "view for position " + entry.getKey() + " at" + entry.getValue(), rect,
142                    notNullValue());
143            assertThat(log + "rect for position " + entry.getKey(), entry.getValue(), is(rect));
144        }
145    }
146
147    @NonNull
148    private String log(Map<Integer, Rect> startPositions, Map<Integer, Rect> endPositions) {
149        StringBuilder logBuilder = new StringBuilder();
150        for (Map.Entry<Integer, Rect> entry : startPositions.entrySet()) {
151            logBuilder.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
152        }
153        logBuilder.append("------\n");
154        for (Map.Entry<Integer, Rect> entry : endPositions.entrySet()) {
155            logBuilder.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
156        }
157        return logBuilder.toString();
158    }
159
160    private Map<Integer, Rect> capturePositions(RecyclerView recyclerView) {
161        Map<Integer, Rect> positions = new HashMap<>();
162        for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
163            View view = mLayoutManager.getChildAt(i);
164            int childAdapterPosition = recyclerView.getChildAdapterPosition(view);
165            Rect outRect = new Rect();
166            mLayoutManager.getDecoratedBoundsWithMargins(view, outRect);
167            // only record if outRect is visible
168            if (outRect.left >= mRecyclerView.getWidth() ||
169                    outRect.top >= mRecyclerView.getHeight() ||
170                    outRect.right < 0 ||
171                    outRect.bottom < 0) {
172                continue;
173            }
174            positions.put(childAdapterPosition, outRect);
175        }
176        return positions;
177    }
178
179    private class StaticAdapter extends RecyclerView.Adapter<TestViewHolder> {
180        final int mSize;
181        // is passed to the layout params of the item
182        final int mMinItemWidth;
183        final int mMinItemHeight;
184
185        public StaticAdapter(int size, int minItemWidth, int minItemHeight) {
186            mSize = size;
187            mMinItemWidth = minItemWidth;
188            mMinItemHeight = minItemHeight;
189        }
190
191        @Override
192        public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
193                int viewType) {
194            return new TestViewHolder(new View(parent.getContext()));
195        }
196
197        @Override
198        public void onBindViewHolder(@NonNull TestViewHolder holder, int position) {
199            holder.mBoundItem = new Item(position, "none");
200            if (mMinItemHeight < 1 && mMinItemWidth < 1) {
201                return;
202            }
203            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
204            if (lp == null) {
205                lp = new ViewGroup.LayoutParams(0, 0);
206            }
207            if (mMinItemWidth > 0) {
208                lp.width = (int) (mMinItemWidth + (position % 10) * mMinItemWidth / 7f);
209            } else {
210                lp.width = mMinItemWidth;
211            }
212
213            if (mMinItemHeight > 0) {
214                lp.height = (int) (mMinItemHeight + (position % 10) * mMinItemHeight / 7f);
215            } else {
216                lp.height = mMinItemHeight;
217            }
218            holder.itemView.setLayoutParams(lp);
219        }
220
221        @Override
222        public int getItemCount() {
223            return mSize;
224        }
225    }
226}
227