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