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.RecyclerView.VERTICAL;
     20 
     21 import static org.junit.Assert.assertFalse;
     22 import static org.junit.Assert.assertNotNull;
     23 import static org.junit.Assert.assertTrue;
     24 
     25 import android.app.Activity;
     26 import android.content.res.Resources;
     27 import android.graphics.Color;
     28 import android.graphics.drawable.StateListDrawable;
     29 import android.support.test.InstrumentationRegistry;
     30 import android.support.test.annotation.UiThreadTest;
     31 import android.support.test.filters.LargeTest;
     32 import android.support.test.runner.AndroidJUnit4;
     33 import android.view.LayoutInflater;
     34 import android.view.MotionEvent;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.view.ViewGroup.LayoutParams;
     38 import android.widget.TextView;
     39 
     40 import androidx.annotation.NonNull;
     41 import androidx.recyclerview.R;
     42 
     43 import org.junit.Test;
     44 import org.junit.runner.RunWith;
     45 
     46 import java.util.concurrent.CountDownLatch;
     47 import java.util.concurrent.TimeUnit;
     48 
     49 @LargeTest
     50 @RunWith(AndroidJUnit4.class)
     51 public class RecyclerViewFastScrollerTest extends BaseRecyclerViewInstrumentationTest {
     52     private static final int FLAG_HORIZONTAL = 1;
     53     private static final int FLAG_VERTICAL = 1 << 1;
     54     private int mScrolledByY = -1000;
     55     private int mScrolledByX = -1000;
     56     private FastScroller mScroller;
     57     private boolean mHide;
     58 
     59     private void setContentView(final int layoutId) throws Throwable {
     60         final Activity activity = mActivityRule.getActivity();
     61         mActivityRule.runOnUiThread(new Runnable() {
     62             @Override
     63             public void run() {
     64                 activity.setContentView(layoutId);
     65             }
     66         });
     67     }
     68 
     69     @Test
     70     public void xml_fastScrollEnabled_startsInvisibleAndAtTop() throws Throwable {
     71         arrangeWithXml();
     72 
     73         assertTrue("Expected centerY to start == 0", mScroller.mVerticalThumbCenterY == 0);
     74         assertFalse("Expected thumb to start invisible", mScroller.isVisible());
     75     }
     76 
     77     @Test
     78     public void scrollBy_displaysAndMovesFastScrollerThumb() throws Throwable {
     79         arrangeWithXml();
     80 
     81         mActivityRule.runOnUiThread(new Runnable() {
     82             @Override
     83             public void run() {
     84                 mRecyclerView.scrollBy(0, 400);
     85             }
     86         });
     87 
     88         assertTrue("Expected centerY to be > 0" + mScroller.mVerticalThumbCenterY,
     89                 mScroller.mVerticalThumbCenterY > 0);
     90         assertTrue("Expected thumb to be visible", mScroller.isVisible());
     91     }
     92 
     93     @Test
     94     public void ui_dragsThumb_scrollsRecyclerView() throws Throwable {
     95         arrangeWithXml();
     96 
     97         // RecyclerView#scrollBy(int, int) used to cause the scroller thumb to show up.
     98         mActivityRule.runOnUiThread(new Runnable() {
     99             @Override
    100             public void run() {
    101                 mRecyclerView.scrollBy(0, 1);
    102                 mRecyclerView.scrollBy(0, -1);
    103             }
    104         });
    105         int[] absoluteCoords = new int[2];
    106         mRecyclerView.getLocationOnScreen(absoluteCoords);
    107         TouchUtils.drag(InstrumentationRegistry.getInstrumentation(), mRecyclerView.getWidth() - 10,
    108                 mRecyclerView.getWidth() - 10, mScroller.mVerticalThumbCenterY + absoluteCoords[1],
    109                 mRecyclerView.getHeight() + absoluteCoords[1], 100);
    110 
    111         assertTrue("Expected dragging thumb to move recyclerView",
    112                 mRecyclerView.computeVerticalScrollOffset() > 0);
    113     }
    114 
    115     @Test
    116     public void properCleanUp() throws Throwable {
    117         mRecyclerView = new RecyclerView(getActivity());
    118         final Activity activity = mActivityRule.getActivity();
    119         final CountDownLatch latch = new CountDownLatch(1);
    120         mActivityRule.runOnUiThread(new Runnable() {
    121             @Override
    122             public void run() {
    123                 activity.setContentView(
    124                         androidx.recyclerview.test.R.layout.fast_scrollbar_test_rv);
    125                 mRecyclerView = (RecyclerView) activity.findViewById(
    126                         androidx.recyclerview.test.R.id.recycler_view);
    127                 LinearLayoutManager layout = new LinearLayoutManager(activity.getBaseContext());
    128                 layout.setOrientation(VERTICAL);
    129                 mRecyclerView.setLayoutManager(layout);
    130                 mRecyclerView.setAdapter(new TestAdapter(50));
    131                 Resources res = getActivity().getResources();
    132                 mScroller = new FastScroller(mRecyclerView, (StateListDrawable) res.getDrawable(
    133                         androidx.recyclerview.test.R.drawable.fast_scroll_thumb_drawable),
    134                         res.getDrawable(
    135                                 androidx.recyclerview.test.R.drawable
    136                                         .fast_scroll_track_drawable),
    137                         (StateListDrawable) res.getDrawable(
    138                                 androidx.recyclerview.test.R.drawable
    139                                         .fast_scroll_thumb_drawable),
    140                         res.getDrawable(
    141                                 androidx.recyclerview.test.R.drawable
    142                                         .fast_scroll_track_drawable),
    143                         res.getDimensionPixelSize(R.dimen.fastscroll_default_thickness),
    144                         res.getDimensionPixelSize(R.dimen.fastscroll_minimum_range),
    145                         res.getDimensionPixelOffset(R.dimen.fastscroll_margin)) {
    146                     @Override
    147                     public void show() {
    148                         // Overriden to avoid animation calls in instrumentation thread
    149                     }
    150 
    151                     @Override
    152                     public void hide(int duration) {
    153                         latch.countDown();
    154                         mHide = true;
    155                     }
    156                 };
    157 
    158             }
    159         });
    160         waitForIdleScroll(mRecyclerView);
    161         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
    162         mActivityRule.runOnUiThread(new Runnable() {
    163             @Override
    164             public void run() {
    165                 mRecyclerView.scrollBy(0, 400);
    166                 mScroller.attachToRecyclerView(new RecyclerView(getActivity()));
    167             }
    168         });
    169         assertFalse(latch.await(2, TimeUnit.SECONDS));
    170         assertFalse(mHide);
    171     }
    172 
    173     @Test
    174     public void inflationTest() throws Throwable {
    175         setContentView(androidx.recyclerview.test.R.layout.fast_scrollbar_test_rv);
    176         getInstrumentation().waitForIdleSync();
    177         RecyclerView view = (RecyclerView) getActivity().findViewById(
    178                 androidx.recyclerview.test.R.id.recycler_view);
    179         assertTrue(view.getItemDecorationCount() == 1);
    180         assertTrue(view.getItemDecorationAt(0) instanceof FastScroller);
    181         FastScroller scroller = (FastScroller) view.getItemDecorationAt(0);
    182         assertNotNull(scroller.getHorizontalThumbDrawable());
    183         assertNotNull(scroller.getHorizontalTrackDrawable());
    184         assertNotNull(scroller.getVerticalThumbDrawable());
    185         assertNotNull(scroller.getVerticalTrackDrawable());
    186     }
    187 
    188     @Test
    189     public void removeFastScrollerSuccessful() throws Throwable {
    190         setContentView(androidx.recyclerview.test.R.layout.fast_scrollbar_test_rv);
    191         getInstrumentation().waitForIdleSync();
    192         final RecyclerView view = (RecyclerView) getActivity().findViewById(
    193                 androidx.recyclerview.test.R.id.recycler_view);
    194         assertTrue(view.getItemDecorationCount() == 1);
    195         mActivityRule.runOnUiThread(new Runnable() {
    196             @Override
    197             public void run() {
    198                 view.removeItemDecorationAt(0);
    199                 assertTrue(view.getItemDecorationCount() == 0);
    200             }
    201         });
    202     }
    203 
    204     @UiThreadTest
    205     @Test
    206     public void initWithBadDrawables() throws Throwable {
    207         arrangeWithCode();
    208 
    209         Throwable exception = null;
    210         try {
    211             mRecyclerView.initFastScroller(null, null, null, null);
    212         } catch (Throwable t) {
    213             exception = t;
    214         }
    215         assertTrue(exception instanceof IllegalArgumentException);
    216     }
    217 
    218     @Test
    219     public void verticalScrollUpdatesFastScrollThumb() throws Throwable {
    220         scrollUpdatesFastScrollThumb(FLAG_VERTICAL);
    221     }
    222 
    223     @Test
    224     public void horizontalScrollUpdatesFastScrollThumb() throws Throwable {
    225         scrollUpdatesFastScrollThumb(FLAG_HORIZONTAL);
    226     }
    227 
    228     private void scrollUpdatesFastScrollThumb(int direction) throws Throwable {
    229         arrangeWithCode();
    230         mScroller.updateScrollPosition(direction == FLAG_VERTICAL ? 0 : 250,
    231                 direction == FLAG_VERTICAL ? 250 : 0);
    232         if (direction == FLAG_VERTICAL) {
    233             assertTrue("Expected 250 for centerY, got " + mScroller.mVerticalThumbCenterY,
    234                     mScroller.mVerticalThumbCenterY == 250);
    235             assertTrue("Expected 250 for thumb height, got " + mScroller.mVerticalThumbHeight,
    236                     mScroller.mVerticalThumbHeight == 250);
    237         } else if (direction == FLAG_HORIZONTAL) {
    238             assertTrue("Expected 250 for centerX, got " + mScroller.mHorizontalThumbCenterX,
    239                     mScroller.mHorizontalThumbCenterX == 250);
    240             assertTrue("Expected 250 for thumb width, got " + mScroller.mHorizontalThumbWidth,
    241                     mScroller.mHorizontalThumbWidth == 250);
    242         }
    243         assertTrue(mScroller.isVisible());
    244 
    245         mScroller.updateScrollPosition(direction == FLAG_VERTICAL ? 0 : 42,
    246                 direction == FLAG_VERTICAL ? 42 : 0);
    247         if (direction == FLAG_VERTICAL) {
    248             assertTrue("Expected 146 for centerY, got " + mScroller.mVerticalThumbCenterY,
    249                     mScroller.mVerticalThumbCenterY == 146);
    250             assertTrue("Expected 250 for thumb height, got " + mScroller.mVerticalThumbHeight,
    251                     mScroller.mVerticalThumbHeight == 250);
    252         } else if (direction == FLAG_HORIZONTAL) {
    253             assertTrue("Expected 146 for centerX, got " + mScroller.mHorizontalThumbCenterX,
    254                     mScroller.mHorizontalThumbCenterX == 146);
    255             assertTrue("Expected 250 for thumb width, got " + mScroller.mHorizontalThumbWidth,
    256                     mScroller.mHorizontalThumbWidth == 250);
    257         }
    258         assertTrue(mScroller.isVisible());
    259     }
    260 
    261     @Test
    262     public void draggingDoesNotTriggerFastScrollIfNotInThumb() throws Throwable {
    263         arrangeWithCode();
    264         mScroller.updateScrollPosition(0, 250);
    265         final MotionEvent downEvent = MotionEvent.obtain(10, 10, MotionEvent.ACTION_DOWN, 250, 250,
    266                 0);
    267         assertFalse(mScroller.onInterceptTouchEvent(mRecyclerView, downEvent));
    268         final MotionEvent moveEvent = MotionEvent.obtain(10, 10, MotionEvent.ACTION_MOVE, 250, 275,
    269                 0);
    270         assertFalse(mScroller.onInterceptTouchEvent(mRecyclerView, moveEvent));
    271     }
    272 
    273     @Test
    274     public void verticalDraggingFastScrollThumbDoesActualScrolling() throws Throwable {
    275         draggingFastScrollThumbDoesActualScrolling(FLAG_VERTICAL);
    276     }
    277 
    278     @Test
    279     public void horizontalDraggingFastScrollThumbDoesActualScrolling() throws Throwable {
    280         draggingFastScrollThumbDoesActualScrolling(FLAG_HORIZONTAL);
    281     }
    282 
    283     private void draggingFastScrollThumbDoesActualScrolling(int direction) throws Throwable {
    284         arrangeWithCode();
    285         mScroller.updateScrollPosition(direction == FLAG_VERTICAL ? 0 : 250,
    286                 direction == FLAG_VERTICAL ? 250 : 0);
    287         final MotionEvent downEvent = MotionEvent.obtain(10, 10, MotionEvent.ACTION_DOWN,
    288                 direction == FLAG_VERTICAL ? 500 : 250, direction == FLAG_VERTICAL ? 250 : 500, 0);
    289         assertTrue(mScroller.onInterceptTouchEvent(mRecyclerView, downEvent));
    290         assertTrue(mScroller.isDragging());
    291         final MotionEvent moveEvent = MotionEvent.obtain(10, 10, MotionEvent.ACTION_MOVE,
    292                 direction == FLAG_VERTICAL ? 500 : 221, direction == FLAG_VERTICAL ? 221 : 500, 0);
    293         mScroller.onTouchEvent(mRecyclerView, moveEvent);
    294         if (direction == FLAG_VERTICAL) {
    295             assertTrue("Expected to get -29, but got " + mScrolledByY, mScrolledByY == -29);
    296         } else {
    297             assertTrue("Expected to get -29, but got " + mScrolledByX, mScrolledByX == -29);
    298         }
    299     }
    300 
    301     private void arrangeWithXml() throws Throwable {
    302 
    303         final TestActivity activity = mActivityRule.getActivity();
    304         final TestedFrameLayout testedFrameLayout = activity.getContainer();
    305 
    306         RecyclerView recyclerView = (RecyclerView) LayoutInflater.from(activity).inflate(
    307                 androidx.recyclerview.test.R.layout.fast_scrollbar_test_rv,
    308                 testedFrameLayout,
    309                 false);
    310 
    311         LinearLayoutManager layout = new LinearLayoutManager(activity.getBaseContext());
    312         layout.setOrientation(VERTICAL);
    313         recyclerView.setLayoutManager(layout);
    314 
    315         recyclerView.setAdapter(new TestAdapter(50));
    316 
    317         mScroller = (FastScroller) recyclerView.getItemDecorationAt(0);
    318 
    319         testedFrameLayout.expectLayouts(1);
    320         testedFrameLayout.expectDraws(1);
    321         setRecyclerView(recyclerView);
    322         testedFrameLayout.waitForLayout(2);
    323         testedFrameLayout.waitForDraw(2);
    324     }
    325 
    326     private void arrangeWithCode() throws Exception {
    327         final int width = 500;
    328         final int height = 500;
    329 
    330         mRecyclerView = new RecyclerView(getActivity()) {
    331             @Override
    332             public int computeVerticalScrollRange() {
    333                 return 1000;
    334             }
    335 
    336             @Override
    337             public int computeVerticalScrollExtent() {
    338                 return 500;
    339             }
    340 
    341             @Override
    342             public int computeVerticalScrollOffset() {
    343                 return 250;
    344             }
    345 
    346             @Override
    347             public int computeHorizontalScrollRange() {
    348                 return 1000;
    349             }
    350 
    351             @Override
    352             public int computeHorizontalScrollExtent() {
    353                 return 500;
    354             }
    355 
    356             @Override
    357             public int computeHorizontalScrollOffset() {
    358                 return 250;
    359             }
    360 
    361             @Override
    362             public void scrollBy(int x, int y) {
    363                 mScrolledByY = y;
    364                 mScrolledByX = x;
    365             }
    366         };
    367         mRecyclerView.setAdapter(new TestAdapter(50));
    368         mRecyclerView.measure(
    369                 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
    370                 View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
    371         mRecyclerView.layout(0, 0, width, height);
    372 
    373         Resources res = getActivity().getResources();
    374         mScroller = new FastScroller(mRecyclerView, (StateListDrawable) res.getDrawable(
    375                 androidx.recyclerview.test.R.drawable.fast_scroll_thumb_drawable),
    376                 res.getDrawable(
    377                         androidx.recyclerview.test.R.drawable.fast_scroll_track_drawable),
    378                 (StateListDrawable) res.getDrawable(
    379                         androidx.recyclerview.test.R.drawable.fast_scroll_thumb_drawable),
    380                 res.getDrawable(
    381                         androidx.recyclerview.test.R.drawable.fast_scroll_track_drawable),
    382                 res.getDimensionPixelSize(R.dimen.fastscroll_default_thickness),
    383                 res.getDimensionPixelSize(R.dimen.fastscroll_minimum_range),
    384                 res.getDimensionPixelOffset(R.dimen.fastscroll_margin)) {
    385             @Override
    386             public void show() {
    387                 // Overriden to avoid animation calls in instrumentation thread
    388             }
    389 
    390             @Override
    391             public void hide(int duration) {
    392                 mHide = true;
    393             }
    394         };
    395         mRecyclerView.mEnableFastScroller = true;
    396 
    397         // Draw it once so height/width gets updated
    398         mScroller.onDrawOver(null, mRecyclerView, null);
    399     }
    400 
    401     private static class TestAdapter extends RecyclerView.Adapter {
    402         private int mItemCount;
    403 
    404         public static class ViewHolder extends RecyclerView.ViewHolder {
    405             public TextView mTextView;
    406 
    407             ViewHolder(TextView v) {
    408                 super(v);
    409                 mTextView = v;
    410             }
    411 
    412             @Override
    413             public String toString() {
    414                 return super.toString() + " '" + mTextView.getText();
    415             }
    416         }
    417 
    418         TestAdapter(int itemCount) {
    419             mItemCount = itemCount;
    420         }
    421 
    422         @Override
    423         public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    424                 int viewType) {
    425             final ViewHolder h = new ViewHolder(new TextView(parent.getContext()));
    426             h.mTextView.setMinimumHeight(128);
    427             h.mTextView.setPadding(20, 0, 20, 0);
    428             h.mTextView.setFocusable(true);
    429             h.mTextView.setBackgroundColor(Color.BLUE);
    430             RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(
    431                     LayoutParams.MATCH_PARENT,
    432                     ViewGroup.LayoutParams.WRAP_CONTENT);
    433             lp.leftMargin = 10;
    434             lp.rightMargin = 5;
    435             lp.topMargin = 20;
    436             lp.bottomMargin = 15;
    437             h.mTextView.setLayoutParams(lp);
    438             return h;
    439         }
    440 
    441         @Override
    442         public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    443             holder.itemView.setTag("pos " + position);
    444         }
    445 
    446         @Override
    447         public int getItemCount() {
    448             return mItemCount;
    449         }
    450     }
    451 }
    452