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, padding.right, padding.bottom); 212 setRecyclerView(recyclerView); 213 recyclerView.waitUntilLayout(); 214 Snapshot snapshot = takeSnapshot(); 215 int index = 0; 216 Rect tmp = new Rect(); 217 for (BaseWrapContentWithAspectRatioTest.MeasureBehavior behavior : adapter.behaviors) { 218 tmp.set(expected[index]); 219 tmp.offset(padding.left, padding.top); 220 assertThat("behavior " + index, snapshot.mChildCoordinates.get(behavior.getId()), 221 is(tmp)); 222 index ++; 223 } 224 Rect boundingBox = new Rect(0, 0, 0, 0); 225 for (Rect rect : expected) { 226 boundingBox.union(rect); 227 } 228 assertThat(recyclerView.getWidth(), is(width + padding.left + padding.right)); 229 assertThat(recyclerView.getHeight(), is(height + padding.top + padding.bottom)); 230 } 231 232 233 abstract protected int getVerticalGravity(RecyclerView.LayoutManager layoutManager); 234 235 abstract protected int getHorizontalGravity(RecyclerView.LayoutManager layoutManager); 236 237 protected Snapshot takeSnapshot() throws Throwable { 238 Snapshot snapshot = new Snapshot(mRecyclerView, mLoggingItemAnimator, 239 getHorizontalGravity(mLayoutManager), getVerticalGravity(mLayoutManager)); 240 return snapshot; 241 } 242 243 abstract class Scenario { 244 245 ArrayList<Step> mStepList = new ArrayList<>(); 246 247 public Scenario(Step... steps) { 248 Collections.addAll(mStepList, steps); 249 } 250 251 public int getSeedAdapterSize() { 252 return 10; 253 } 254 255 public RecyclerView.LayoutManager createLayoutManager() { 256 return BaseWrapContentTest.this.createLayoutManager(); 257 } 258 } 259 260 abstract static class Step { 261 262 abstract void onRun() throws Throwable; 263 } 264 265 class Snapshot { 266 267 Rect mRawChildrenBox = new Rect(); 268 269 Rect mRvSize = new Rect(); 270 271 Rect mRvPadding = new Rect(); 272 273 Rect mRvParentSize = new Rect(); 274 275 LongSparseArray<Rect> mChildCoordinates = new LongSparseArray<>(); 276 277 LongSparseArray<String> mAppear = new LongSparseArray<>(); 278 279 LongSparseArray<String> mDisappear = new LongSparseArray<>(); 280 281 LongSparseArray<String> mPersistent = new LongSparseArray<>(); 282 283 LongSparseArray<String> mChanged = new LongSparseArray<>(); 284 285 int mVerticalGravity; 286 287 int mHorizontalGravity; 288 289 int mOffsetX, mOffsetY;// how much we should offset children 290 291 public Snapshot(RecyclerView recyclerView, LoggingItemAnimator loggingItemAnimator, 292 int horizontalGravity, int verticalGravity) 293 throws Throwable { 294 mRvSize = getViewBounds(recyclerView); 295 mRvParentSize = getViewBounds((View) recyclerView.getParent()); 296 mRvPadding = new Rect(recyclerView.getPaddingLeft(), recyclerView.getPaddingTop(), 297 recyclerView.getPaddingRight(), recyclerView.getPaddingBottom()); 298 mVerticalGravity = verticalGravity; 299 mHorizontalGravity = horizontalGravity; 300 if (mVerticalGravity == Gravity.TOP) { 301 mOffsetY = 0; 302 } else { 303 mOffsetY = mRvParentSize.bottom - mRvSize.bottom; 304 } 305 306 if (mHorizontalGravity == Gravity.LEFT) { 307 mOffsetX = 0; 308 } else { 309 mOffsetX = mRvParentSize.right - mRvSize.right; 310 } 311 collectChildCoordinates(recyclerView); 312 if (loggingItemAnimator != null) { 313 collectInto(mAppear, loggingItemAnimator.mAnimateAppearanceList); 314 collectInto(mDisappear, loggingItemAnimator.mAnimateDisappearanceList); 315 collectInto(mPersistent, loggingItemAnimator.mAnimatePersistenceList); 316 collectInto(mChanged, loggingItemAnimator.mAnimateChangeList); 317 } 318 } 319 320 public boolean doesChildrenFitVertically() { 321 return mRawChildrenBox.top >= mRvPadding.top 322 && mRawChildrenBox.bottom <= mRvSize.bottom - mRvPadding.bottom; 323 } 324 325 public boolean doesChildrenFitHorizontally() { 326 return mRawChildrenBox.left >= mRvPadding.left 327 && mRawChildrenBox.right <= mRvSize.right - mRvPadding.right; 328 } 329 330 public void assertSame(Snapshot other, int step) { 331 if (mWrapContentConfig.isUnlimitedHeight() && 332 (!doesChildrenFitVertically() || !other.doesChildrenFitVertically())) { 333 if (DEBUG) { 334 Log.d(TAG, "cannot assert coordinates because it does not fit vertically"); 335 } 336 return; 337 } 338 if (mWrapContentConfig.isUnlimitedWidth() && 339 (!doesChildrenFitHorizontally() || !other.doesChildrenFitHorizontally())) { 340 if (DEBUG) { 341 Log.d(TAG, "cannot assert coordinates because it does not fit horizontally"); 342 } 343 return; 344 } 345 assertMap("child coordinates. step:" + step, mChildCoordinates, 346 other.mChildCoordinates); 347 if (mWrapContentConfig.isUnlimitedHeight() || mWrapContentConfig.isUnlimitedWidth()) { 348 return;//cannot assert animatinos in unlimited size 349 } 350 assertMap("appearing step:" + step, mAppear, other.mAppear); 351 assertMap("disappearing step:" + step, mDisappear, other.mDisappear); 352 assertMap("persistent step:" + step, mPersistent, other.mPersistent); 353 assertMap("changed step:" + step, mChanged, other.mChanged); 354 } 355 356 private void assertMap(String prefix, LongSparseArray<?> map1, LongSparseArray<?> map2) { 357 StringBuilder logBuilder = new StringBuilder(); 358 logBuilder.append(prefix).append("\n"); 359 logBuilder.append("map1").append("\n"); 360 logInto(map1, logBuilder); 361 logBuilder.append("map2").append("\n"); 362 logInto(map2, logBuilder); 363 final String log = logBuilder.toString(); 364 assertEquals(log + " same size", map1.size(), map2.size()); 365 for (int i = 0; i < map1.size(); i++) { 366 assertAtIndex(log, map1, map2, i); 367 } 368 } 369 370 private void assertAtIndex(String prefix, LongSparseArray<?> map1, LongSparseArray<?> map2, 371 int index) { 372 long key1 = map1.keyAt(index); 373 long key2 = map2.keyAt(index); 374 assertEquals(prefix + "key mismatch at index " + index, key1, key2); 375 Object value1 = map1.valueAt(index); 376 Object value2 = map2.valueAt(index); 377 assertEquals(prefix + " value mismatch at index " + index, value1, value2); 378 } 379 380 private void logInto(LongSparseArray<?> map, StringBuilder sb) { 381 for (int i = 0; i < map.size(); i++) { 382 long key = map.keyAt(i); 383 Object value = map.valueAt(i); 384 sb.append(key).append(" : ").append(value).append("\n"); 385 } 386 } 387 388 @Override 389 public String toString() { 390 StringBuilder sb = new StringBuilder("Snapshot{\n"); 391 sb.append("child coordinates:\n"); 392 logInto(mChildCoordinates, sb); 393 sb.append("appear animations:\n"); 394 logInto(mAppear, sb); 395 sb.append("disappear animations:\n"); 396 logInto(mDisappear, sb); 397 sb.append("change animations:\n"); 398 logInto(mChanged, sb); 399 sb.append("persistent animations:\n"); 400 logInto(mPersistent, sb); 401 sb.append("}"); 402 return sb.toString(); 403 } 404 405 @Override 406 public int hashCode() { 407 int result = mChildCoordinates.hashCode(); 408 result = 31 * result + mAppear.hashCode(); 409 result = 31 * result + mDisappear.hashCode(); 410 result = 31 * result + mPersistent.hashCode(); 411 result = 31 * result + mChanged.hashCode(); 412 return result; 413 } 414 415 private void collectInto( 416 LongSparseArray<String> target, 417 List<? extends BaseRecyclerViewAnimationsTest.AnimateLogBase> list) { 418 for (BaseRecyclerViewAnimationsTest.AnimateLogBase base : list) { 419 long id = getItemId(base.viewHolder); 420 assertNull(target.get(id)); 421 target.put(id, log(base)); 422 } 423 } 424 425 private String log(BaseRecyclerViewAnimationsTest.AnimateLogBase base) { 426 return base.getClass().getSimpleName() + 427 ((TextView) base.viewHolder.itemView).getText() + ": " + 428 "[pre:" + log(base.postInfo) + 429 ", post:" + log(base.postInfo) + "]"; 430 } 431 432 private String log(BaseRecyclerViewAnimationsTest.LoggingInfo postInfo) { 433 if (postInfo == null) { 434 return "?"; 435 } 436 return "PI[flags: " + postInfo.changeFlags 437 + ",l:" + (postInfo.left + mOffsetX) 438 + ",t:" + (postInfo.top + mOffsetY) 439 + ",r:" + (postInfo.right + mOffsetX) 440 + ",b:" + (postInfo.bottom + mOffsetY) + "]"; 441 } 442 443 void collectChildCoordinates(RecyclerView recyclerView) throws Throwable { 444 mRawChildrenBox = new Rect(0, 0, 0, 0); 445 final int childCount = recyclerView.getChildCount(); 446 for (int i = 0; i < childCount; i++) { 447 View child = recyclerView.getChildAt(i); 448 Rect childBounds = getChildBounds(recyclerView, child, true); 449 mRawChildrenBox.union(getChildBounds(recyclerView, child, false)); 450 RecyclerView.ViewHolder childViewHolder = recyclerView.getChildViewHolder(child); 451 mChildCoordinates.put(getItemId(childViewHolder), childBounds); 452 } 453 } 454 455 private Rect getViewBounds(View view) { 456 return new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); 457 } 458 459 private Rect getChildBounds(RecyclerView recyclerView, View child, boolean offset) { 460 RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); 461 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); 462 Rect rect = new Rect(layoutManager.getDecoratedLeft(child) - lp.leftMargin, 463 layoutManager.getDecoratedTop(child) - lp.topMargin, 464 layoutManager.getDecoratedRight(child) + lp.rightMargin, 465 layoutManager.getDecoratedBottom(child) + lp.bottomMargin); 466 if (offset) { 467 rect.offset(mOffsetX, mOffsetY); 468 } 469 return rect; 470 } 471 472 private long getItemId(RecyclerView.ViewHolder vh) { 473 if (vh instanceof TestViewHolder) { 474 return ((TestViewHolder) vh).mBoundItem.mId; 475 } else if (vh instanceof BaseWrapContentWithAspectRatioTest.WrapContentViewHolder) { 476 BaseWrapContentWithAspectRatioTest.WrapContentViewHolder casted = 477 (BaseWrapContentWithAspectRatioTest.WrapContentViewHolder) vh; 478 return casted.mView.mBehavior.getId(); 479 } else { 480 throw new IllegalArgumentException("i don't support any VH"); 481 } 482 } 483 484 public void assertRvSize() { 485 if (shouldWrapContentHorizontally()) { 486 int expectedW = mRawChildrenBox.width() + mRvPadding.left + mRvPadding.right; 487 assertTrue(mRvSize.width() + " <= " + expectedW, mRvSize.width() <= expectedW); 488 } 489 if (shouldWrapContentVertically()) { 490 int expectedH = mRawChildrenBox.height() + mRvPadding.top + mRvPadding.bottom; 491 assertTrue(mRvSize.height() + "<=" + expectedH, mRvSize.height() <= expectedH); 492 } 493 } 494 } 495 496 protected boolean shouldWrapContentHorizontally() { 497 return true; 498 } 499 500 protected boolean shouldWrapContentVertically() { 501 return true; 502 } 503 504 static class WrappedRecyclerView extends RecyclerView { 505 506 public WrappedRecyclerView(Context context) { 507 super(context); 508 } 509 510 public void waitUntilLayout() { 511 while (isLayoutRequested()) { 512 try { 513 Thread.sleep(100); 514 } catch (InterruptedException e) { 515 e.printStackTrace(); 516 } 517 } 518 } 519 520 public void waitUntilAnimations() throws InterruptedException { 521 final CountDownLatch latch = new CountDownLatch(1); 522 if (mItemAnimator == null || !mItemAnimator.isRunning( 523 new ItemAnimator.ItemAnimatorFinishedListener() { 524 @Override 525 public void onAnimationsFinished() { 526 latch.countDown(); 527 } 528 })) { 529 latch.countDown(); 530 } 531 MatcherAssert.assertThat("waiting too long for animations", 532 latch.await(60, TimeUnit.SECONDS), CoreMatchers.is(true)); 533 } 534 535 @Override 536 protected void onLayout(boolean changed, int l, int t, int r, int b) { 537 super.onLayout(changed, l, t, r, b); 538 } 539 } 540 541 static class WrapContentConfig { 542 543 public boolean unlimitedWidth; 544 public boolean unlimitedHeight; 545 public Rect padding = new Rect(0, 0, 0, 0); 546 547 public WrapContentConfig(boolean unlimitedWidth, boolean unlimitedHeight) { 548 this.unlimitedWidth = unlimitedWidth; 549 this.unlimitedHeight = unlimitedHeight; 550 } 551 552 public WrapContentConfig(boolean unlimitedWidth, boolean unlimitedHeight, Rect padding) { 553 this.unlimitedWidth = unlimitedWidth; 554 this.unlimitedHeight = unlimitedHeight; 555 this.padding.set(padding); 556 } 557 558 public boolean isUnlimitedWidth() { 559 return unlimitedWidth; 560 } 561 562 public WrapContentConfig setUnlimitedWidth(boolean unlimitedWidth) { 563 this.unlimitedWidth = unlimitedWidth; 564 return this; 565 } 566 567 public boolean isUnlimitedHeight() { 568 return unlimitedHeight; 569 } 570 571 public WrapContentConfig setUnlimitedHeight(boolean unlimitedHeight) { 572 this.unlimitedHeight = unlimitedHeight; 573 return this; 574 } 575 576 @Override 577 public String toString() { 578 return "WrapContentConfig{" 579 + "unlimitedWidth=" + unlimitedWidth 580 + ", unlimitedHeight=" + unlimitedHeight 581 + ", padding=" + padding 582 + '}'; 583 } 584 585 public TestedFrameLayout.FullControlLayoutParams toLayoutParams(int wDim, int hDim) { 586 TestedFrameLayout.FullControlLayoutParams 587 lp = new TestedFrameLayout.FullControlLayoutParams( 588 wDim, hDim); 589 if (unlimitedWidth) { 590 lp.wSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 591 } 592 if (unlimitedHeight) { 593 lp.hSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 594 } 595 return lp; 596 } 597 } 598 } 599