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