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 17package android.support.v7.widget.helper; 18 19import android.app.Instrumentation; 20import android.os.Debug; 21import android.os.SystemClock; 22import android.support.v4.view.ViewCompat; 23import android.support.v7.widget.BaseRecyclerViewInstrumentationTest; 24import android.support.v7.widget.RecyclerView; 25import android.support.v7.widget.WrappedRecyclerView; 26import android.test.InstrumentationTestCase; 27import android.view.Gravity; 28import android.view.MotionEvent; 29import android.view.View; 30import android.view.ViewConfiguration; 31import android.view.ViewGroup; 32 33import java.util.ArrayList; 34import java.util.List; 35 36import static android.support.v7.widget.helper.ItemTouchHelper.*; 37 38public class ItemTouchHelperTest extends BaseRecyclerViewInstrumentationTest { 39 40 TestAdapter mAdapter; 41 42 TestLayoutManager mLayoutManager; 43 44 private LoggingCalback mCalback; 45 46 private LoggingItemTouchHelper mItemTouchHelper; 47 48 private WrappedRecyclerView mWrappedRecyclerView; 49 50 private Boolean mSetupRTL; 51 52 public ItemTouchHelperTest() { 53 super(false); 54 } 55 56 private RecyclerView setup(int dragDirs, int swipeDirs) throws Throwable { 57 mWrappedRecyclerView = inflateWrappedRV(); 58 mAdapter = new TestAdapter(10); 59 mLayoutManager = new TestLayoutManager() { 60 @Override 61 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 62 detachAndScrapAttachedViews(recycler); 63 layoutRange(recycler, 0, Math.min(5, state.getItemCount())); 64 layoutLatch.countDown(); 65 } 66 67 @Override 68 public boolean canScrollHorizontally() { 69 return false; 70 } 71 72 @Override 73 public boolean supportsPredictiveItemAnimations() { 74 return false; 75 } 76 }; 77 mWrappedRecyclerView.setFakeRTL(mSetupRTL); 78 mWrappedRecyclerView.setAdapter(mAdapter); 79 mWrappedRecyclerView.setLayoutManager(mLayoutManager); 80 mCalback = new LoggingCalback(dragDirs, swipeDirs); 81 mItemTouchHelper = new LoggingItemTouchHelper(mCalback); 82 runTestOnUiThread(new Runnable() { 83 @Override 84 public void run() { 85 mItemTouchHelper.attachToRecyclerView(mWrappedRecyclerView); 86 } 87 }); 88 89 return mWrappedRecyclerView; 90 } 91 92 public void testSwipeLeft() throws Throwable { 93 basicSwipeTest(LEFT, LEFT | RIGHT, -getActivity().getWindow().getDecorView().getWidth()); 94 } 95 96 public void testSwipeRight() throws Throwable { 97 basicSwipeTest(RIGHT, LEFT | RIGHT, getActivity().getWindow().getDecorView().getWidth()); 98 } 99 100 public void testSwipeStart() throws Throwable { 101 basicSwipeTest(START, START | END, -getActivity().getWindow().getDecorView().getWidth()); 102 } 103 104 public void testSwipeEnd() throws Throwable { 105 basicSwipeTest(END, START | END, getActivity().getWindow().getDecorView().getWidth()); 106 } 107 108 public void testSwipeStartInRTL() throws Throwable { 109 mSetupRTL = true; 110 basicSwipeTest(START, START | END, getActivity().getWindow().getDecorView().getWidth()); 111 } 112 113 public void testSwipeEndInRTL() throws Throwable { 114 mSetupRTL = true; 115 basicSwipeTest(END, START | END, -getActivity().getWindow().getDecorView().getWidth()); 116 } 117 118 private void setLayoutDirection(final View view, final int layoutDir) throws Throwable { 119 runTestOnUiThread(new Runnable() { 120 @Override 121 public void run() { 122 ViewCompat.setLayoutDirection(view, layoutDir); 123 } 124 }); 125 } 126 127 public void basicSwipeTest(int dir, int swipeDirs, int targetX) throws Throwable { 128 final RecyclerView recyclerView = setup(0, swipeDirs); 129 mLayoutManager.expectLayouts(1); 130 setRecyclerView(recyclerView); 131 mLayoutManager.waitForLayout(1); 132 133 final RecyclerView.ViewHolder target = mRecyclerView 134 .findViewHolderForAdapterPosition(1); 135 TouchUtils.dragViewToX(this, target.itemView, Gravity.CENTER, targetX); 136 Thread.sleep(100); //wait for animation end 137 final SwipeRecord swipe = mCalback.getSwipe(target); 138 assertNotNull(swipe); 139 assertEquals(dir, swipe.dir); 140 assertEquals(1, mItemTouchHelper.mRecoverAnimations.size()); 141 assertEquals(1, mItemTouchHelper.mPendingCleanup.size()); 142 // get rid of the view 143 mLayoutManager.expectLayouts(1); 144 mAdapter.deleteAndNotify(1, 1); 145 mLayoutManager.waitForLayout(1); 146 waitForAnimations(); 147 assertEquals(0, mItemTouchHelper.mRecoverAnimations.size()); 148 assertEquals(0, mItemTouchHelper.mPendingCleanup.size()); 149 assertTrue(mCalback.isCleared(target)); 150 } 151 152 private void waitForAnimations() throws InterruptedException { 153 while (mRecyclerView.getItemAnimator().isRunning()) { 154 Thread.sleep(100); 155 } 156 } 157 158 private static class LoggingCalback extends SimpleCallback { 159 160 private List<MoveRecord> mMoveRecordList = new ArrayList<MoveRecord>(); 161 162 private List<SwipeRecord> mSwipeRecords = new ArrayList<SwipeRecord>(); 163 164 private List<RecyclerView.ViewHolder> mCleared = new ArrayList<RecyclerView.ViewHolder>(); 165 166 public LoggingCalback(int dragDirs, int swipeDirs) { 167 super(dragDirs, swipeDirs); 168 } 169 170 @Override 171 public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, 172 RecyclerView.ViewHolder target) { 173 mMoveRecordList.add(new MoveRecord(viewHolder, target)); 174 return true; 175 } 176 177 @Override 178 public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { 179 mSwipeRecords.add(new SwipeRecord(viewHolder, direction)); 180 } 181 182 public MoveRecord getMove(RecyclerView.ViewHolder vh) { 183 for (MoveRecord move : mMoveRecordList) { 184 if (move.from == vh) { 185 return move; 186 } 187 } 188 return null; 189 } 190 191 @Override 192 public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 193 super.clearView(recyclerView, viewHolder); 194 mCleared.add(viewHolder); 195 } 196 197 public SwipeRecord getSwipe(RecyclerView.ViewHolder vh) { 198 for (SwipeRecord swipe : mSwipeRecords) { 199 if (swipe.viewHolder == vh) { 200 return swipe; 201 } 202 } 203 return null; 204 } 205 206 public boolean isCleared(RecyclerView.ViewHolder vh) { 207 return mCleared.contains(vh); 208 } 209 } 210 211 private static class LoggingItemTouchHelper extends ItemTouchHelper { 212 213 public LoggingItemTouchHelper(Callback callback) { 214 super(callback); 215 } 216 } 217 218 private static class SwipeRecord { 219 220 RecyclerView.ViewHolder viewHolder; 221 222 int dir; 223 224 public SwipeRecord(RecyclerView.ViewHolder viewHolder, int dir) { 225 this.viewHolder = viewHolder; 226 this.dir = dir; 227 } 228 } 229 230 private static class MoveRecord { 231 232 final int fromPos, toPos; 233 234 RecyclerView.ViewHolder from, to; 235 236 public MoveRecord(RecyclerView.ViewHolder from, 237 RecyclerView.ViewHolder to) { 238 this.from = from; 239 this.to = to; 240 fromPos = from.getAdapterPosition(); 241 toPos = to.getAdapterPosition(); 242 } 243 } 244 245 246 /** 247 * RecyclerView specific TouchUtils. 248 */ 249 static class TouchUtils { 250 251 /** 252 * Simulate touching the center of a view and releasing quickly (before the tap timeout). 253 * 254 * @param test The test case that is being run 255 * @param v The view that should be clicked 256 */ 257 public static void tapView(InstrumentationTestCase test, RecyclerView recyclerView, 258 View v) { 259 int[] xy = new int[2]; 260 v.getLocationOnScreen(xy); 261 262 final int viewWidth = v.getWidth(); 263 final int viewHeight = v.getHeight(); 264 265 final float x = xy[0] + (viewWidth / 2.0f); 266 float y = xy[1] + (viewHeight / 2.0f); 267 268 long downTime = SystemClock.uptimeMillis(); 269 long eventTime = SystemClock.uptimeMillis(); 270 271 MotionEvent event = MotionEvent.obtain(downTime, eventTime, 272 MotionEvent.ACTION_DOWN, x, y, 0); 273 Instrumentation inst = test.getInstrumentation(); 274 inst.sendPointerSync(event); 275 inst.waitForIdleSync(); 276 277 eventTime = SystemClock.uptimeMillis(); 278 final int touchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); 279 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, 280 x + (touchSlop / 2.0f), y + (touchSlop / 2.0f), 0); 281 inst.sendPointerSync(event); 282 inst.waitForIdleSync(); 283 284 eventTime = SystemClock.uptimeMillis(); 285 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); 286 inst.sendPointerSync(event); 287 inst.waitForIdleSync(); 288 } 289 290 /** 291 * Simulate touching the center of a view and cancelling (so no onClick should 292 * fire, etc). 293 * 294 * @param test The test case that is being run 295 * @param v The view that should be clicked 296 */ 297 public static void touchAndCancelView(InstrumentationTestCase test, View v) { 298 int[] xy = new int[2]; 299 v.getLocationOnScreen(xy); 300 301 final int viewWidth = v.getWidth(); 302 final int viewHeight = v.getHeight(); 303 304 final float x = xy[0] + (viewWidth / 2.0f); 305 float y = xy[1] + (viewHeight / 2.0f); 306 307 Instrumentation inst = test.getInstrumentation(); 308 309 long downTime = SystemClock.uptimeMillis(); 310 long eventTime = SystemClock.uptimeMillis(); 311 312 MotionEvent event = MotionEvent.obtain(downTime, eventTime, 313 MotionEvent.ACTION_DOWN, x, y, 0); 314 inst.sendPointerSync(event); 315 inst.waitForIdleSync(); 316 317 eventTime = SystemClock.uptimeMillis(); 318 final int touchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); 319 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_CANCEL, 320 x + (touchSlop / 2.0f), y + (touchSlop / 2.0f), 0); 321 inst.sendPointerSync(event); 322 inst.waitForIdleSync(); 323 324 } 325 326 /** 327 * Simulate touching the center of a view and releasing. 328 * 329 * @param test The test case that is being run 330 * @param v The view that should be clicked 331 */ 332 public static void clickView(InstrumentationTestCase test, View v) { 333 int[] xy = new int[2]; 334 v.getLocationOnScreen(xy); 335 336 final int viewWidth = v.getWidth(); 337 final int viewHeight = v.getHeight(); 338 339 final float x = xy[0] + (viewWidth / 2.0f); 340 float y = xy[1] + (viewHeight / 2.0f); 341 342 Instrumentation inst = test.getInstrumentation(); 343 344 long downTime = SystemClock.uptimeMillis(); 345 long eventTime = SystemClock.uptimeMillis(); 346 347 MotionEvent event = MotionEvent.obtain(downTime, eventTime, 348 MotionEvent.ACTION_DOWN, x, y, 0); 349 inst.sendPointerSync(event); 350 inst.waitForIdleSync(); 351 352 eventTime = SystemClock.uptimeMillis(); 353 final int touchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); 354 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, 355 x + (touchSlop / 2.0f), y + (touchSlop / 2.0f), 0); 356 inst.sendPointerSync(event); 357 inst.waitForIdleSync(); 358 359 eventTime = SystemClock.uptimeMillis(); 360 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); 361 inst.sendPointerSync(event); 362 inst.waitForIdleSync(); 363 364 try { 365 Thread.sleep(1000); 366 } catch (InterruptedException e) { 367 e.printStackTrace(); 368 } 369 } 370 371 /** 372 * Simulate touching the center of a view, holding until it is a long press, and then 373 * releasing. 374 * 375 * @param test The test case that is being run 376 * @param v The view that should be clicked 377 */ 378 public static void longClickView(InstrumentationTestCase test, View v) { 379 int[] xy = new int[2]; 380 v.getLocationOnScreen(xy); 381 382 final int viewWidth = v.getWidth(); 383 final int viewHeight = v.getHeight(); 384 385 final float x = xy[0] + (viewWidth / 2.0f); 386 float y = xy[1] + (viewHeight / 2.0f); 387 388 Instrumentation inst = test.getInstrumentation(); 389 390 long downTime = SystemClock.uptimeMillis(); 391 long eventTime = SystemClock.uptimeMillis(); 392 393 MotionEvent event = MotionEvent.obtain(downTime, eventTime, 394 MotionEvent.ACTION_DOWN, x, y, 0); 395 inst.sendPointerSync(event); 396 inst.waitForIdleSync(); 397 398 eventTime = SystemClock.uptimeMillis(); 399 final int touchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); 400 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, 401 x + touchSlop / 2, y + touchSlop / 2, 0); 402 inst.sendPointerSync(event); 403 inst.waitForIdleSync(); 404 405 try { 406 Thread.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f)); 407 } catch (InterruptedException e) { 408 e.printStackTrace(); 409 } 410 411 eventTime = SystemClock.uptimeMillis(); 412 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); 413 inst.sendPointerSync(event); 414 inst.waitForIdleSync(); 415 } 416 417 /** 418 * Simulate touching the center of a view and dragging to the top of the screen. 419 * 420 * @param test The test case that is being run 421 * @param v The view that should be dragged 422 */ 423 public static void dragViewToTop(InstrumentationTestCase test, View v) { 424 dragViewToTop(test, v, 4); 425 } 426 427 /** 428 * Simulate touching the center of a view and dragging to the top of the screen. 429 * 430 * @param test The test case that is being run 431 * @param v The view that should be dragged 432 * @param stepCount How many move steps to include in the drag 433 */ 434 public static void dragViewToTop(InstrumentationTestCase test, View v, int stepCount) { 435 int[] xy = new int[2]; 436 v.getLocationOnScreen(xy); 437 438 final int viewWidth = v.getWidth(); 439 final int viewHeight = v.getHeight(); 440 441 final float x = xy[0] + (viewWidth / 2.0f); 442 float fromY = xy[1] + (viewHeight / 2.0f); 443 float toY = 0; 444 445 drag(test, x, x, fromY, toY, stepCount); 446 } 447 448 /** 449 * Get the location of a view. Use the gravity param to specify which part of the view to 450 * return. 451 * 452 * @param v View to find 453 * @param gravity A combination of (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, 454 * CENTER_HORIZONTAL, 455 * RIGHT) 456 * @param xy Result 457 */ 458 private static void getStartLocation(View v, int gravity, int[] xy) { 459 v.getLocationOnScreen(xy); 460 461 final int viewWidth = v.getWidth(); 462 final int viewHeight = v.getHeight(); 463 464 switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) { 465 case Gravity.TOP: 466 break; 467 case Gravity.CENTER_VERTICAL: 468 xy[1] += viewHeight / 2; 469 break; 470 case Gravity.BOTTOM: 471 xy[1] += viewHeight - 1; 472 break; 473 default: 474 // Same as top -- do nothing 475 } 476 477 switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 478 case Gravity.LEFT: 479 break; 480 case Gravity.CENTER_HORIZONTAL: 481 xy[0] += viewWidth / 2; 482 break; 483 case Gravity.RIGHT: 484 xy[0] += viewWidth - 1; 485 break; 486 default: 487 // Same as left -- do nothing 488 } 489 } 490 491 /** 492 * Simulate touching a view and dragging it to a specified location. 493 * 494 * @param test The test case that is being run 495 * @param v The view that should be dragged 496 * @param gravity Which part of the view to use for the initial down event. A combination 497 * of 498 * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) 499 * @param toX Final location of the view after dragging 500 * @param toY Final location of the view after dragging 501 * @return distance in pixels covered by the drag 502 */ 503 public static int dragViewTo(InstrumentationTestCase test, View v, int gravity, int toX, 504 int toY) { 505 int[] xy = new int[2]; 506 507 getStartLocation(v, gravity, xy); 508 509 final int fromX = xy[0]; 510 final int fromY = xy[1]; 511 512 int deltaX = fromX - toX; 513 int deltaY = fromY - toY; 514 515 int distance = (int) Math.sqrt(deltaX * deltaX + deltaY * deltaY); 516 drag(test, fromX, toX, fromY, toY, distance); 517 518 return distance; 519 } 520 521 /** 522 * Simulate touching a view and dragging it to a specified location. Only moves 523 * horizontally. 524 * 525 * @param test The test case that is being run 526 * @param v The view that should be dragged 527 * @param gravity Which part of the view to use for the initial down event. A combination 528 * of 529 * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) 530 * @param toX Final location of the view after dragging 531 * @return distance in pixels covered by the drag 532 */ 533 public static int dragViewToX(InstrumentationTestCase test, View v, int gravity, int toX) { 534 int[] xy = new int[2]; 535 536 getStartLocation(v, gravity, xy); 537 538 final int fromX = xy[0]; 539 final int fromY = xy[1]; 540 541 int deltaX = fromX - toX; 542 543 drag(test, fromX, toX, fromY, fromY, Math.max(10, Math.abs(deltaX) / 10)); 544 545 return deltaX; 546 } 547 548 /** 549 * Simulate touching a view and dragging it to a specified location. Only moves vertically. 550 * 551 * @param test The test case that is being run 552 * @param v The view that should be dragged 553 * @param gravity Which part of the view to use for the initial down event. A combination 554 * of 555 * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) 556 * @param toY Final location of the view after dragging 557 * @return distance in pixels covered by the drag 558 */ 559 public static int dragViewToY(InstrumentationTestCase test, View v, int gravity, int toY) { 560 int[] xy = new int[2]; 561 562 getStartLocation(v, gravity, xy); 563 564 final int fromX = xy[0]; 565 final int fromY = xy[1]; 566 567 int deltaY = fromY - toY; 568 569 drag(test, fromX, fromX, fromY, toY, deltaY); 570 571 return deltaY; 572 } 573 574 575 /** 576 * Simulate touching a specific location and dragging to a new location. 577 * 578 * @param test The test case that is being run 579 * @param fromX X coordinate of the initial touch, in screen coordinates 580 * @param toX Xcoordinate of the drag destination, in screen coordinates 581 * @param fromY X coordinate of the initial touch, in screen coordinates 582 * @param toY Y coordinate of the drag destination, in screen coordinates 583 * @param stepCount How many move steps to include in the drag 584 */ 585 public static void drag(InstrumentationTestCase test, float fromX, float toX, float fromY, 586 float toY, int stepCount) { 587 Instrumentation inst = test.getInstrumentation(); 588 589 long downTime = SystemClock.uptimeMillis(); 590 long eventTime = SystemClock.uptimeMillis(); 591 592 float y = fromY; 593 float x = fromX; 594 595 float yStep = (toY - fromY) / stepCount; 596 float xStep = (toX - fromX) / stepCount; 597 598 MotionEvent event = MotionEvent.obtain(downTime, eventTime, 599 MotionEvent.ACTION_DOWN, x, y, 0); 600 inst.sendPointerSync(event); 601 for (int i = 0; i < stepCount; ++i) { 602 y += yStep; 603 x += xStep; 604 eventTime = SystemClock.uptimeMillis(); 605 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0); 606 inst.sendPointerSync(event); 607 } 608 609 eventTime = SystemClock.uptimeMillis(); 610 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); 611 inst.sendPointerSync(event); 612 inst.waitForIdleSync(); 613 } 614 } 615 616} 617