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