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