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