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