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 */ 16 17package android.support.v7.widget; 18 19 20import org.junit.Test; 21import org.junit.runner.RunWith; 22import org.junit.runners.Parameterized; 23 24import android.graphics.Rect; 25import android.support.v4.view.ViewCompat; 26import android.test.suitebuilder.annotation.MediumTest; 27import android.util.Log; 28import android.view.View; 29import android.view.ViewGroup; 30import android.view.ViewParent; 31 32import java.util.ArrayList; 33import java.util.List; 34import java.util.Map; 35 36import static android.support.v7.widget.LayoutState.LAYOUT_END; 37import static android.support.v7.widget.LayoutState.LAYOUT_START; 38import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL; 39import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 40 41import static org.hamcrest.CoreMatchers.hasItem; 42import static org.hamcrest.CoreMatchers.is; 43import static org.hamcrest.CoreMatchers.not; 44import static org.hamcrest.CoreMatchers.sameInstance; 45import static org.junit.Assert.assertEquals; 46import static org.junit.Assert.assertNotNull; 47import static org.junit.Assert.assertThat; 48 49import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 50import static android.view.ViewGroup.LayoutParams.FILL_PARENT; 51 52/** 53 * Tests that rely on the basic configuration and does not do any additions / removals 54 */ 55@RunWith(Parameterized.class) 56@MediumTest 57public class LinearLayoutManagerBaseConfigSetTest extends BaseLinearLayoutManagerTest { 58 59 private final Config mConfig; 60 61 public LinearLayoutManagerBaseConfigSetTest(Config config) { 62 mConfig = config; 63 } 64 65 66 @Parameterized.Parameters(name = "{0}") 67 public static List<Config> configs() throws CloneNotSupportedException { 68 List<Config> result = new ArrayList<>(); 69 for (Config config : createBaseVariations()) { 70 result.add(config); 71 } 72 return result; 73 } 74 75 @Test 76 public void scrollToPositionWithOffsetTest() throws Throwable { 77 Config config = ((Config) mConfig.clone()).itemCount(300); 78 setupByConfig(config, true); 79 OrientationHelper orientationHelper = OrientationHelper 80 .createOrientationHelper(mLayoutManager, config.mOrientation); 81 Rect layoutBounds = getDecoratedRecyclerViewBounds(); 82 // try scrolling towards head, should not affect anything 83 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 84 if (config.mStackFromEnd) { 85 scrollToPositionWithOffset(mTestAdapter.getItemCount() - 1, 86 mLayoutManager.mOrientationHelper.getEnd() - 500); 87 } else { 88 scrollToPositionWithOffset(0, 20); 89 } 90 assertRectSetsEqual(config + " trying to over scroll with offset should be no-op", 91 before, mLayoutManager.collectChildCoordinates()); 92 // try offsetting some visible children 93 int testCount = 10; 94 while (testCount-- > 0) { 95 // get middle child 96 final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2); 97 final int position = mRecyclerView.getChildLayoutPosition(child); 98 final int startOffset = config.mReverseLayout ? 99 orientationHelper.getEndAfterPadding() - orientationHelper 100 .getDecoratedEnd(child) 101 : orientationHelper.getDecoratedStart(child) - orientationHelper 102 .getStartAfterPadding(); 103 final int scrollOffset = config.mStackFromEnd ? startOffset + startOffset / 2 104 : startOffset / 2; 105 mLayoutManager.expectLayouts(1); 106 scrollToPositionWithOffset(position, scrollOffset); 107 mLayoutManager.waitForLayout(2); 108 final int finalOffset = config.mReverseLayout ? 109 orientationHelper.getEndAfterPadding() - orientationHelper 110 .getDecoratedEnd(child) 111 : orientationHelper.getDecoratedStart(child) - orientationHelper 112 .getStartAfterPadding(); 113 assertEquals(config + " scroll with offset on a visible child should work fine " + 114 " offset:" + finalOffset + " , existing offset:" + startOffset + ", " 115 + "child " + position, 116 scrollOffset, finalOffset); 117 } 118 119 // try scrolling to invisible children 120 testCount = 10; 121 // we test above and below, one by one 122 int offsetMultiplier = -1; 123 while (testCount-- > 0) { 124 final TargetTuple target = findInvisibleTarget(config); 125 final String logPrefix = config + " " + target; 126 mLayoutManager.expectLayouts(1); 127 final int offset = offsetMultiplier 128 * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3; 129 scrollToPositionWithOffset(target.mPosition, offset); 130 mLayoutManager.waitForLayout(2); 131 final View child = mLayoutManager.findViewByPosition(target.mPosition); 132 assertNotNull(logPrefix + " scrolling to a mPosition with offset " + offset 133 + " should layout it", child); 134 final Rect bounds = mLayoutManager.getViewBounds(child); 135 if (DEBUG) { 136 Log.d(TAG, logPrefix + " post scroll to invisible mPosition " + bounds + " in " 137 + layoutBounds + " with offset " + offset); 138 } 139 140 if (config.mReverseLayout) { 141 assertEquals(logPrefix + " when scrolling with offset to an invisible in reverse " 142 + "layout, its end should align with recycler view's end - offset", 143 orientationHelper.getEndAfterPadding() - offset, 144 orientationHelper.getDecoratedEnd(child) 145 ); 146 } else { 147 assertEquals( 148 logPrefix + " when scrolling with offset to an invisible child in normal" 149 + " layout its start should align with recycler view's start + " 150 + "offset", 151 orientationHelper.getStartAfterPadding() + offset, 152 orientationHelper.getDecoratedStart(child) 153 ); 154 } 155 offsetMultiplier *= -1; 156 } 157 } 158 159 @Test 160 public void getFirstLastChildrenTest() throws Throwable { 161 final Config config = ((Config) mConfig.clone()).itemCount(300); 162 setupByConfig(config, true); 163 Runnable viewInBoundsTest = new Runnable() { 164 @Override 165 public void run() { 166 VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren(); 167 final String boundsLog = mLayoutManager.getBoundsLog(); 168 assertEquals(config + ":\nfirst visible child should match traversal result\n" 169 + boundsLog, visibleChildren.firstVisiblePosition, 170 mLayoutManager.findFirstVisibleItemPosition() 171 ); 172 assertEquals( 173 config + ":\nfirst fully visible child should match traversal result\n" 174 + boundsLog, visibleChildren.firstFullyVisiblePosition, 175 mLayoutManager.findFirstCompletelyVisibleItemPosition() 176 ); 177 178 assertEquals(config + ":\nlast visible child should match traversal result\n" 179 + boundsLog, visibleChildren.lastVisiblePosition, 180 mLayoutManager.findLastVisibleItemPosition() 181 ); 182 assertEquals( 183 config + ":\nlast fully visible child should match traversal result\n" 184 + boundsLog, visibleChildren.lastFullyVisiblePosition, 185 mLayoutManager.findLastCompletelyVisibleItemPosition() 186 ); 187 } 188 }; 189 runTestOnUiThread(viewInBoundsTest); 190 // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching 191 // case 192 final int scrollPosition = config.mStackFromEnd ? 0 : mTestAdapter.getItemCount(); 193 runTestOnUiThread(new Runnable() { 194 @Override 195 public void run() { 196 mRecyclerView.smoothScrollToPosition(scrollPosition); 197 } 198 }); 199 while (mLayoutManager.isSmoothScrolling() || 200 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 201 runTestOnUiThread(viewInBoundsTest); 202 Thread.sleep(400); 203 } 204 // delete all items 205 mLayoutManager.expectLayouts(2); 206 mTestAdapter.deleteAndNotify(0, mTestAdapter.getItemCount()); 207 mLayoutManager.waitForLayout(2); 208 // test empty case 209 runTestOnUiThread(viewInBoundsTest); 210 // set a new adapter with huge items to test full bounds check 211 mLayoutManager.expectLayouts(1); 212 final int totalSpace = mLayoutManager.mOrientationHelper.getTotalSpace(); 213 final TestAdapter newAdapter = new TestAdapter(100) { 214 @Override 215 public void onBindViewHolder(TestViewHolder holder, 216 int position) { 217 super.onBindViewHolder(holder, position); 218 if (config.mOrientation == HORIZONTAL) { 219 holder.itemView.setMinimumWidth(totalSpace + 5); 220 } else { 221 holder.itemView.setMinimumHeight(totalSpace + 5); 222 } 223 } 224 }; 225 runTestOnUiThread(new Runnable() { 226 @Override 227 public void run() { 228 mRecyclerView.setAdapter(newAdapter); 229 } 230 }); 231 mLayoutManager.waitForLayout(2); 232 runTestOnUiThread(viewInBoundsTest); 233 } 234 235 @Test 236 public void dontRecycleViewsTranslatedOutOfBoundsFromStart() throws Throwable { 237 final Config config = ((Config) mConfig.clone()).itemCount(1000); 238 setupByConfig(config, true); 239 mLayoutManager.expectLayouts(1); 240 scrollToPosition(500); 241 mLayoutManager.waitForLayout(2); 242 final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(500); 243 OrientationHelper helper = mLayoutManager.mOrientationHelper; 244 int gap = helper.getDecoratedStart(vh.itemView); 245 scrollBy(gap); 246 gap = helper.getDecoratedStart(vh.itemView); 247 assertThat("test sanity", gap, is(0)); 248 249 final int size = helper.getDecoratedMeasurement(vh.itemView); 250 AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView); 251 runTestOnUiThread(new Runnable() { 252 @Override 253 public void run() { 254 if (mConfig.mOrientation == HORIZONTAL) { 255 ViewCompat.setTranslationX(vh.itemView, size * 2); 256 } else { 257 ViewCompat.setTranslationY(vh.itemView, size * 2); 258 } 259 } 260 }); 261 scrollBy(size * 2); 262 assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView)))); 263 assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView)); 264 assertThat(vh.getAdapterPosition(), is(500)); 265 scrollBy(size * 2); 266 assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView))); 267 } 268 269 @Test 270 public void dontRecycleViewsTranslatedOutOfBoundsFromEnd() throws Throwable { 271 final Config config = ((Config) mConfig.clone()).itemCount(1000); 272 setupByConfig(config, true); 273 mLayoutManager.expectLayouts(1); 274 scrollToPosition(500); 275 mLayoutManager.waitForLayout(2); 276 final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(500); 277 OrientationHelper helper = mLayoutManager.mOrientationHelper; 278 int gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView); 279 scrollBy(-gap); 280 gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView); 281 assertThat("test sanity", gap, is(0)); 282 283 final int size = helper.getDecoratedMeasurement(vh.itemView); 284 AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView); 285 runTestOnUiThread(new Runnable() { 286 @Override 287 public void run() { 288 if (mConfig.mOrientation == HORIZONTAL) { 289 ViewCompat.setTranslationX(vh.itemView, -size * 2); 290 } else { 291 ViewCompat.setTranslationY(vh.itemView, -size * 2); 292 } 293 } 294 }); 295 scrollBy(-size * 2); 296 assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView)))); 297 assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView)); 298 assertThat(vh.getAdapterPosition(), is(500)); 299 scrollBy(-size * 2); 300 assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView))); 301 } 302 303 private TargetTuple findInvisibleTarget(Config config) { 304 int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE; 305 for (int i = 0; i < mLayoutManager.getChildCount(); i++) { 306 View child = mLayoutManager.getChildAt(i); 307 int position = mRecyclerView.getChildLayoutPosition(child); 308 if (position < minPosition) { 309 minPosition = position; 310 } 311 if (position > maxPosition) { 312 maxPosition = position; 313 } 314 } 315 final int tailTarget = maxPosition + 316 (mRecyclerView.getAdapter().getItemCount() - maxPosition) / 2; 317 final int headTarget = minPosition / 2; 318 final int target; 319 // where will the child come from ? 320 final int itemLayoutDirection; 321 if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) { 322 target = tailTarget; 323 itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END; 324 } else { 325 target = headTarget; 326 itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START; 327 } 328 if (DEBUG) { 329 Log.d(TAG, 330 config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition); 331 } 332 return new TargetTuple(target, itemLayoutDirection); 333 } 334} 335