Home | History | Annotate | Download | only in widget
      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 }
    327