Justin Klaassen | 4d01eea | 2018-04-03 23:21:57 -0400 | [diff] [blame^] | 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 | |
| 17 | package androidx.recyclerview.widget; |
| 18 | |
| 19 | import static androidx.recyclerview.widget.ItemTouchHelper.END; |
| 20 | import static androidx.recyclerview.widget.ItemTouchHelper.LEFT; |
| 21 | import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT; |
| 22 | import static androidx.recyclerview.widget.ItemTouchHelper.START; |
| 23 | import static androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback; |
| 24 | |
| 25 | import static org.junit.Assert.assertEquals; |
| 26 | import static org.junit.Assert.assertNotNull; |
| 27 | import static org.junit.Assert.assertTrue; |
| 28 | |
| 29 | import android.os.Build; |
| 30 | import android.support.test.filters.LargeTest; |
| 31 | import android.support.test.filters.SdkSuppress; |
| 32 | import android.support.test.filters.Suppress; |
| 33 | import android.support.test.runner.AndroidJUnit4; |
| 34 | import android.view.Gravity; |
| 35 | import android.view.View; |
| 36 | |
| 37 | import androidx.annotation.NonNull; |
| 38 | import androidx.core.util.Pair; |
| 39 | import androidx.testutils.PollingCheck; |
| 40 | |
| 41 | import org.junit.Test; |
| 42 | import org.junit.runner.RunWith; |
| 43 | |
| 44 | import java.util.ArrayList; |
| 45 | import java.util.List; |
| 46 | |
| 47 | @LargeTest |
| 48 | @RunWith(AndroidJUnit4.class) |
| 49 | public 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 | } |