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