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