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 */ 16 17 package androidx.recyclerview.widget; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertTrue; 23 24 import android.support.test.filters.LargeTest; 25 import android.support.test.runner.AndroidJUnit4; 26 import android.util.Log; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.TextView; 30 31 import androidx.annotation.NonNull; 32 33 import org.junit.Before; 34 import org.junit.Test; 35 import org.junit.runner.RunWith; 36 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.HashSet; 40 import java.util.List; 41 import java.util.Set; 42 import java.util.concurrent.Semaphore; 43 import java.util.concurrent.TimeUnit; 44 45 @LargeTest 46 @RunWith(AndroidJUnit4.class) 47 public class DefaultItemAnimatorTest extends BaseRecyclerViewInstrumentationTest { 48 49 private static final String TAG = "DefaultItemAnimatorTest"; 50 Throwable mainThreadException; 51 52 DefaultItemAnimator mAnimator; 53 Adapter mAdapter; 54 ViewGroup mDummyParent; 55 List<RecyclerView.ViewHolder> mExpectedItems = new ArrayList<RecyclerView.ViewHolder>(); 56 57 Set<RecyclerView.ViewHolder> mRemoveFinished = new HashSet<RecyclerView.ViewHolder>(); 58 Set<RecyclerView.ViewHolder> mAddFinished = new HashSet<RecyclerView.ViewHolder>(); 59 Set<RecyclerView.ViewHolder> mMoveFinished = new HashSet<RecyclerView.ViewHolder>(); 60 Set<RecyclerView.ViewHolder> mChangeFinished = new HashSet<RecyclerView.ViewHolder>(); 61 62 Semaphore mExpectedItemCount = new Semaphore(0); 63 64 @Before 65 public void setUp() throws Exception { 66 mAnimator = new DefaultItemAnimator() { 67 @Override 68 public void onRemoveFinished(RecyclerView.ViewHolder item) { 69 try { 70 assertTrue(mRemoveFinished.add(item)); 71 onFinished(item); 72 } catch (Throwable t) { 73 postExceptionToInstrumentation(t); 74 } 75 } 76 77 @Override 78 public void onAddFinished(RecyclerView.ViewHolder item) { 79 try { 80 assertTrue(mAddFinished.add(item)); 81 onFinished(item); 82 } catch (Throwable t) { 83 postExceptionToInstrumentation(t); 84 } 85 } 86 87 @Override 88 public void onMoveFinished(RecyclerView.ViewHolder item) { 89 try { 90 assertTrue(mMoveFinished.add(item)); 91 onFinished(item); 92 } catch (Throwable t) { 93 postExceptionToInstrumentation(t); 94 } 95 } 96 97 @Override 98 public void onChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) { 99 try { 100 assertTrue(mChangeFinished.add(item)); 101 onFinished(item); 102 } catch (Throwable t) { 103 postExceptionToInstrumentation(t); 104 } 105 } 106 107 private void onFinished(RecyclerView.ViewHolder item) { 108 assertNotNull(mExpectedItems.remove(item)); 109 mExpectedItemCount.release(1); 110 } 111 }; 112 mAdapter = new Adapter(20); 113 mDummyParent = getActivity().getContainer(); 114 } 115 116 @Override 117 void checkForMainThreadException() throws Throwable { 118 if (mainThreadException != null) { 119 throw mainThreadException; 120 } 121 } 122 123 @Test 124 public void reUseWithPayload() { 125 RecyclerView.ViewHolder vh = new ViewHolder(new TextView(getActivity())); 126 assertFalse(mAnimator.canReuseUpdatedViewHolder(vh, new ArrayList<>())); 127 assertTrue(mAnimator.canReuseUpdatedViewHolder(vh, Arrays.asList((Object) "a"))); 128 } 129 130 void expectItems(RecyclerView.ViewHolder... viewHolders) { 131 mExpectedItems.addAll(Arrays.asList(viewHolders)); 132 } 133 134 void runAndWait(int itemCount, int seconds) throws Throwable { 135 runAndWait(itemCount, seconds, null); 136 } 137 138 void runAndWait(int itemCount, int seconds, final ThrowingRunnable postRun) throws Throwable { 139 mActivityRule.runOnUiThread(new Runnable() { 140 @Override 141 public void run() { 142 mAnimator.runPendingAnimations(); 143 if (postRun != null) { 144 try { 145 postRun.run(); 146 } catch (Throwable e) { 147 throw new RuntimeException(e); 148 } 149 } 150 } 151 }); 152 waitForItems(itemCount, seconds); 153 checkForMainThreadException(); 154 } 155 156 void waitForItems(int itemCount, int seconds) throws InterruptedException { 157 assertTrue("all vh animations should end", 158 mExpectedItemCount.tryAcquire(itemCount, seconds, TimeUnit.SECONDS)); 159 assertEquals("all expected finish events should happen", 0, mExpectedItems.size()); 160 // wait one more second for unwanted 161 assertFalse("should not receive any more permits", 162 mExpectedItemCount.tryAcquire(1, 2, TimeUnit.SECONDS)); 163 } 164 165 @Test 166 public void animateAdd() throws Throwable { 167 ViewHolder vh = createViewHolder(1); 168 expectItems(vh); 169 assertTrue(animateAdd(vh)); 170 assertTrue(mAnimator.isRunning()); 171 runAndWait(1, 1); 172 } 173 174 @Test 175 public void animateRemove() throws Throwable { 176 ViewHolder vh = createViewHolder(1); 177 expectItems(vh); 178 assertTrue(animateRemove(vh)); 179 assertTrue(mAnimator.isRunning()); 180 runAndWait(1, 1); 181 } 182 183 @Test 184 public void animateMove() throws Throwable { 185 ViewHolder vh = createViewHolder(1); 186 expectItems(vh); 187 assertTrue(animateMove(vh, 0, 0, 100, 100)); 188 assertTrue(mAnimator.isRunning()); 189 runAndWait(1, 1); 190 } 191 192 @Test 193 public void animateChange() throws Throwable { 194 ViewHolder vh = createViewHolder(1); 195 ViewHolder vh2 = createViewHolder(2); 196 expectItems(vh, vh2); 197 assertTrue(animateChange(vh, vh2, 0, 0, 100, 100)); 198 assertTrue(mAnimator.isRunning()); 199 runAndWait(2, 1); 200 } 201 202 public void cancelBefore(int count, final RecyclerView.ViewHolder... toCancel) 203 throws Throwable { 204 cancelTest(true, count, toCancel); 205 } 206 207 public void cancelAfter(int count, final RecyclerView.ViewHolder... toCancel) 208 throws Throwable { 209 cancelTest(false, count, toCancel); 210 } 211 212 public void cancelTest(boolean before, int count, final RecyclerView.ViewHolder... toCancel) throws Throwable { 213 if (before) { 214 endAnimations(toCancel); 215 runAndWait(count, 1); 216 } else { 217 runAndWait(count, 1, new ThrowingRunnable() { 218 @Override 219 public void run() throws Throwable { 220 endAnimations(toCancel); 221 } 222 }); 223 } 224 } 225 226 @Test 227 public void cancelAddBefore() throws Throwable { 228 final ViewHolder vh = createViewHolder(1); 229 expectItems(vh); 230 assertTrue(animateAdd(vh)); 231 cancelBefore(1, vh); 232 } 233 234 @Test 235 public void cancelAddAfter() throws Throwable { 236 final ViewHolder vh = createViewHolder(1); 237 expectItems(vh); 238 assertTrue(animateAdd(vh)); 239 cancelAfter(1, vh); 240 } 241 242 @Test 243 public void cancelMoveBefore() throws Throwable { 244 ViewHolder vh = createViewHolder(1); 245 expectItems(vh); 246 assertTrue(animateMove(vh, 10, 10, 100, 100)); 247 cancelBefore(1, vh); 248 } 249 250 @Test 251 public void cancelMoveAfter() throws Throwable { 252 ViewHolder vh = createViewHolder(1); 253 expectItems(vh); 254 assertTrue(animateMove(vh, 10, 10, 100, 100)); 255 cancelAfter(1, vh); 256 } 257 258 @Test 259 public void cancelRemove() throws Throwable { 260 ViewHolder vh = createViewHolder(1); 261 expectItems(vh); 262 assertTrue(animateRemove(vh)); 263 endAnimations(vh); 264 runAndWait(1, 1); 265 } 266 267 @Test 268 public void cancelChangeOldBefore() throws Throwable { 269 cancelChangeOldTest(true); 270 } 271 @Test 272 public void cancelChangeOldAfter() throws Throwable { 273 cancelChangeOldTest(false); 274 } 275 276 public void cancelChangeOldTest(boolean before) throws Throwable { 277 ViewHolder vh = createViewHolder(1); 278 ViewHolder vh2 = createViewHolder(1); 279 expectItems(vh, vh2); 280 assertTrue(animateChange(vh, vh2, 20, 20, 100, 100)); 281 cancelTest(before, 2, vh); 282 } 283 284 @Test 285 public void cancelChangeNewBefore() throws Throwable { 286 cancelChangeNewTest(true); 287 } 288 289 @Test 290 public void cancelChangeNewAfter() throws Throwable { 291 cancelChangeNewTest(false); 292 } 293 294 public void cancelChangeNewTest(boolean before) throws Throwable { 295 ViewHolder vh = createViewHolder(1); 296 ViewHolder vh2 = createViewHolder(1); 297 expectItems(vh, vh2); 298 assertTrue(animateChange(vh, vh2, 20, 20, 100, 100)); 299 cancelTest(before, 2, vh2); 300 } 301 302 @Test 303 public void cancelChangeBothBefore() throws Throwable { 304 cancelChangeBothTest(true); 305 } 306 307 @Test 308 public void cancelChangeBothAfter() throws Throwable { 309 cancelChangeBothTest(false); 310 } 311 312 public void cancelChangeBothTest(boolean before) throws Throwable { 313 ViewHolder vh = createViewHolder(1); 314 ViewHolder vh2 = createViewHolder(1); 315 expectItems(vh, vh2); 316 assertTrue(animateChange(vh, vh2, 20, 20, 100, 100)); 317 cancelTest(before, 2, vh, vh2); 318 } 319 320 void endAnimations(final RecyclerView.ViewHolder... vhs) throws Throwable { 321 mActivityRule.runOnUiThread(new Runnable() { 322 @Override 323 public void run() { 324 for (RecyclerView.ViewHolder vh : vhs) { 325 mAnimator.endAnimation(vh); 326 } 327 } 328 }); 329 } 330 331 boolean animateAdd(final RecyclerView.ViewHolder vh) throws Throwable { 332 final boolean[] result = new boolean[1]; 333 mActivityRule.runOnUiThread(new Runnable() { 334 @Override 335 public void run() { 336 result[0] = mAnimator.animateAdd(vh); 337 } 338 }); 339 return result[0]; 340 } 341 342 boolean animateRemove(final RecyclerView.ViewHolder vh) throws Throwable { 343 final boolean[] result = new boolean[1]; 344 mActivityRule.runOnUiThread(new Runnable() { 345 @Override 346 public void run() { 347 result[0] = mAnimator.animateRemove(vh); 348 } 349 }); 350 return result[0]; 351 } 352 353 boolean animateMove(final RecyclerView.ViewHolder vh, final int fromX, final int fromY, 354 final int toX, final int toY) throws Throwable { 355 final boolean[] result = new boolean[1]; 356 mActivityRule.runOnUiThread(new Runnable() { 357 @Override 358 public void run() { 359 result[0] = mAnimator.animateMove(vh, fromX, fromY, toX, toY); 360 } 361 }); 362 return result[0]; 363 } 364 365 boolean animateChange(final RecyclerView.ViewHolder oldHolder, 366 final RecyclerView.ViewHolder newHolder, 367 final int fromX, final int fromY, final int toX, final int toY) throws Throwable { 368 final boolean[] result = new boolean[1]; 369 mActivityRule.runOnUiThread(new Runnable() { 370 @Override 371 public void run() { 372 result[0] = mAnimator.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY); 373 } 374 }); 375 return result[0]; 376 } 377 378 private ViewHolder createViewHolder(final int pos) throws Throwable { 379 final ViewHolder vh = mAdapter.createViewHolder(mDummyParent, 1); 380 mActivityRule.runOnUiThread(new Runnable() { 381 @Override 382 public void run() { 383 mAdapter.bindViewHolder(vh, pos); 384 mDummyParent.addView(vh.itemView); 385 } 386 }); 387 388 return vh; 389 } 390 391 @Override 392 void postExceptionToInstrumentation(Throwable t) { 393 if (mainThreadException == null) { 394 mainThreadException = t; 395 } else { 396 Log.e(TAG, "skipping secondary main thread exception", t); 397 } 398 } 399 400 401 private class Adapter extends RecyclerView.Adapter<ViewHolder> { 402 403 List<String> mItems; 404 405 private Adapter(int count) { 406 mItems = new ArrayList<>(); 407 for (int i = 0; i < count; i++) { 408 mItems.add("item-" + i); 409 } 410 } 411 412 @Override 413 public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 414 return new ViewHolder(new TextView(parent.getContext())); 415 } 416 417 @Override 418 public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 419 holder.bind(mItems.get(position)); 420 } 421 422 @Override 423 public int getItemCount() { 424 return mItems.size(); 425 } 426 } 427 428 private class ViewHolder extends RecyclerView.ViewHolder { 429 430 String mBindedText; 431 432 public ViewHolder(View itemView) { 433 super(itemView); 434 } 435 436 public void bind(String text) { 437 mBindedText = text; 438 ((TextView) itemView).setText(text); 439 } 440 } 441 442 private interface ThrowingRunnable { 443 void run() throws Throwable; 444 } 445 } 446