ItemTouchHelperTest.java revision ac5fe7c617c66850fff75a9fce9979c6e5674b0f
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
17package androidx.recyclerview.widget;
18
19import static androidx.recyclerview.widget.ItemTouchHelper.END;
20import static androidx.recyclerview.widget.ItemTouchHelper.LEFT;
21import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
22import static androidx.recyclerview.widget.ItemTouchHelper.START;
23import static androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback;
24
25import static org.junit.Assert.assertEquals;
26import static org.junit.Assert.assertNotNull;
27import static org.junit.Assert.assertTrue;
28
29import android.os.Build;
30import androidx.annotation.NonNull;
31import android.support.test.filters.LargeTest;
32import android.support.test.filters.SdkSuppress;
33import android.support.test.filters.Suppress;
34import android.support.test.runner.AndroidJUnit4;
35import androidx.core.util.Pair;
36
37import android.view.Gravity;
38import android.view.View;
39
40import org.junit.Test;
41import org.junit.runner.RunWith;
42
43import java.util.ArrayList;
44import java.util.List;
45
46import androidx.testutils.PollingCheck;
47
48@LargeTest
49@RunWith(AndroidJUnit4.class)
50public class ItemTouchHelperTest extends BaseRecyclerViewInstrumentationTest {
51
52    private static class RecyclerViewState {
53        public TestAdapter mAdapter;
54        public TestLayoutManager mLayoutManager;
55        public WrappedRecyclerView mWrappedRecyclerView;
56    }
57
58    private LoggingCalback mCalback;
59
60    private LoggingItemTouchHelper mItemTouchHelper;
61
62    private Boolean mSetupRTL;
63
64    public ItemTouchHelperTest() {
65        super(false);
66    }
67
68    private RecyclerViewState setupRecyclerView() throws Throwable {
69        RecyclerViewState rvs = new RecyclerViewState();
70        rvs.mWrappedRecyclerView = inflateWrappedRV();
71        rvs.mAdapter = new TestAdapter(10);
72        rvs.mLayoutManager = new TestLayoutManager() {
73            @Override
74            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
75                detachAndScrapAttachedViews(recycler);
76                layoutRange(recycler, 0, Math.min(5, state.getItemCount()));
77                layoutLatch.countDown();
78            }
79
80            @Override
81            public boolean canScrollHorizontally() {
82                return false;
83            }
84
85            @Override
86            public boolean supportsPredictiveItemAnimations() {
87                return false;
88            }
89        };
90        rvs.mWrappedRecyclerView.setFakeRTL(mSetupRTL);
91        rvs.mWrappedRecyclerView.setAdapter(rvs.mAdapter);
92        rvs.mWrappedRecyclerView.setLayoutManager(rvs.mLayoutManager);
93        return rvs;
94    }
95
96    private RecyclerViewState setupItemTouchHelper(final RecyclerViewState rvs, int dragDirs,
97            int swipeDirs) throws Throwable {
98        mCalback = new LoggingCalback(dragDirs, swipeDirs);
99        mItemTouchHelper = new LoggingItemTouchHelper(mCalback);
100        mActivityRule.runOnUiThread(new Runnable() {
101            @Override
102            public void run() {
103                mItemTouchHelper.attachToRecyclerView(rvs.mWrappedRecyclerView);
104            }
105        });
106
107        return rvs;
108    }
109
110    @Test
111    public void swipeLeft() throws Throwable {
112        basicSwipeTest(LEFT, LEFT | RIGHT, -getActivity().getWindow().getDecorView().getWidth());
113    }
114
115    @Test
116    public void swipeRight() throws Throwable {
117        basicSwipeTest(RIGHT, LEFT | RIGHT, getActivity().getWindow().getDecorView().getWidth());
118    }
119
120    @Test
121    public void swipeStart() throws Throwable {
122        basicSwipeTest(START, START | END, -getActivity().getWindow().getDecorView().getWidth());
123    }
124
125    @Test
126    public void swipeEnd() throws Throwable {
127        basicSwipeTest(END, START | END, getActivity().getWindow().getDecorView().getWidth());
128    }
129
130    // Test is disabled as it is flaky.
131    @Suppress
132    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
133    @Test
134    public void swipeStartInRTL() throws Throwable {
135        mSetupRTL = true;
136        basicSwipeTest(START, START | END, getActivity().getWindow().getDecorView().getWidth());
137    }
138
139    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
140    @Test
141    public void swipeEndInRTL() throws Throwable {
142        mSetupRTL = true;
143        basicSwipeTest(END, START | END, -getActivity().getWindow().getDecorView().getWidth());
144    }
145
146    @Test
147    public void attachToNullRecycleViewDuringLongPress() throws Throwable {
148        final RecyclerViewState rvs = setupItemTouchHelper(setupRecyclerView(), END, 0);
149        rvs.mLayoutManager.expectLayouts(1);
150        setRecyclerView(rvs.mWrappedRecyclerView);
151        rvs.mLayoutManager.waitForLayout(1);
152
153        final RecyclerView.ViewHolder target = mRecyclerView
154                .findViewHolderForAdapterPosition(1);
155        target.itemView.setOnLongClickListener(new View.OnLongClickListener() {
156            @Override
157            public boolean onLongClick(View v) {
158                mItemTouchHelper.attachToRecyclerView(null);
159                return false;
160            }
161        });
162        TouchUtils.longClickView(getInstrumentation(), target.itemView);
163    }
164
165    @Test
166    public void attachToAnotherRecycleViewDuringLongPress() throws Throwable {
167        final RecyclerViewState rvs2 = setupRecyclerView();
168        rvs2.mLayoutManager.expectLayouts(1);
169        mActivityRule.runOnUiThread(new Runnable() {
170            @Override
171            public void run() {
172                getActivity().getContainer().addView(rvs2.mWrappedRecyclerView);
173            }
174        });
175        rvs2.mLayoutManager.waitForLayout(1);
176
177        final RecyclerViewState rvs = setupItemTouchHelper(setupRecyclerView(), END, 0);
178        rvs.mLayoutManager.expectLayouts(1);
179        setRecyclerView(rvs.mWrappedRecyclerView);
180        rvs.mLayoutManager.waitForLayout(1);
181
182        final RecyclerView.ViewHolder target = mRecyclerView
183                .findViewHolderForAdapterPosition(1);
184        target.itemView.setOnLongClickListener(new View.OnLongClickListener() {
185            @Override
186            public boolean onLongClick(View v) {
187                mItemTouchHelper.attachToRecyclerView(rvs2.mWrappedRecyclerView);
188                return false;
189            }
190        });
191        TouchUtils.longClickView(getInstrumentation(), target.itemView);
192        assertEquals(0, mCalback.mHasDragFlag.size());
193    }
194
195    public void basicSwipeTest(int dir, int swipeDirs, int targetX) throws Throwable {
196        final RecyclerViewState rvs = setupItemTouchHelper(setupRecyclerView(), 0, swipeDirs);
197        rvs.mLayoutManager.expectLayouts(1);
198        setRecyclerView(rvs.mWrappedRecyclerView);
199        rvs.mLayoutManager.waitForLayout(1);
200
201        final RecyclerView.ViewHolder target = mRecyclerView
202                .findViewHolderForAdapterPosition(1);
203        TouchUtils.dragViewToX(getInstrumentation(), target.itemView, Gravity.CENTER, targetX);
204
205        PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
206            @Override
207            public boolean canProceed() {
208                return mCalback.getSwipe(target) != null;
209            }
210        });
211        final SwipeRecord swipe = mCalback.getSwipe(target);
212        assertNotNull(swipe);
213        assertEquals(dir, swipe.dir);
214        assertEquals(1, mItemTouchHelper.mRecoverAnimations.size());
215        assertEquals(1, mItemTouchHelper.mPendingCleanup.size());
216        // get rid of the view
217        rvs.mLayoutManager.expectLayouts(1);
218        rvs.mAdapter.deleteAndNotify(1, 1);
219        rvs.mLayoutManager.waitForLayout(1);
220        waitForAnimations();
221        assertEquals(0, mItemTouchHelper.mRecoverAnimations.size());
222        assertEquals(0, mItemTouchHelper.mPendingCleanup.size());
223        assertTrue(mCalback.isCleared(target));
224    }
225
226    private void waitForAnimations() throws InterruptedException {
227        while (mRecyclerView.getItemAnimator().isRunning()) {
228            Thread.sleep(100);
229        }
230    }
231
232    private static class LoggingCalback extends SimpleCallback {
233
234        private List<MoveRecord> mMoveRecordList = new ArrayList<MoveRecord>();
235
236        private List<SwipeRecord> mSwipeRecords = new ArrayList<SwipeRecord>();
237
238        private List<RecyclerView.ViewHolder> mCleared = new ArrayList<RecyclerView.ViewHolder>();
239
240        public List<Pair<RecyclerView, RecyclerView.ViewHolder>> mHasDragFlag = new ArrayList<>();
241
242        LoggingCalback(int dragDirs, int swipeDirs) {
243            super(dragDirs, swipeDirs);
244        }
245
246        @Override
247        public boolean onMove(@NonNull RecyclerView recyclerView,
248                @NonNull RecyclerView.ViewHolder viewHolder,
249                @NonNull RecyclerView.ViewHolder target) {
250            mMoveRecordList.add(new MoveRecord(viewHolder, target));
251            return true;
252        }
253
254        @Override
255        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
256            mSwipeRecords.add(new SwipeRecord(viewHolder, direction));
257        }
258
259        public MoveRecord getMove(RecyclerView.ViewHolder vh) {
260            for (MoveRecord move : mMoveRecordList) {
261                if (move.from == vh) {
262                    return move;
263                }
264            }
265            return null;
266        }
267
268        @Override
269        public void clearView(@NonNull RecyclerView recyclerView,
270                @NonNull RecyclerView.ViewHolder viewHolder) {
271            super.clearView(recyclerView, viewHolder);
272            mCleared.add(viewHolder);
273        }
274
275        @Override
276        boolean hasDragFlag(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
277            mHasDragFlag.add(new Pair<>(recyclerView, viewHolder));
278            return super.hasDragFlag(recyclerView, viewHolder);
279        }
280
281        public SwipeRecord getSwipe(RecyclerView.ViewHolder vh) {
282            for (SwipeRecord swipe : mSwipeRecords) {
283                if (swipe.viewHolder == vh) {
284                    return swipe;
285                }
286            }
287            return null;
288        }
289
290        public boolean isCleared(RecyclerView.ViewHolder vh) {
291            return mCleared.contains(vh);
292        }
293    }
294
295    private static class LoggingItemTouchHelper extends ItemTouchHelper {
296
297        public LoggingItemTouchHelper(Callback callback) {
298            super(callback);
299        }
300    }
301
302    private static class SwipeRecord {
303
304        RecyclerView.ViewHolder viewHolder;
305
306        int dir;
307
308        public SwipeRecord(RecyclerView.ViewHolder viewHolder, int dir) {
309            this.viewHolder = viewHolder;
310            this.dir = dir;
311        }
312    }
313
314    private static class MoveRecord {
315
316        final int fromPos, toPos;
317
318        RecyclerView.ViewHolder from, to;
319
320        MoveRecord(RecyclerView.ViewHolder from, RecyclerView.ViewHolder to) {
321            this.from = from;
322            this.to = to;
323            fromPos = from.getAdapterPosition();
324            toPos = to.getAdapterPosition();
325        }
326    }
327}
328