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.RecyclerView.VERTICAL;
20
21import static org.junit.Assert.assertFalse;
22import static org.junit.Assert.assertNotNull;
23import static org.junit.Assert.assertTrue;
24
25import android.app.Activity;
26import android.content.res.Resources;
27import android.graphics.Color;
28import android.graphics.drawable.StateListDrawable;
29import android.support.test.InstrumentationRegistry;
30import android.support.test.annotation.UiThreadTest;
31import android.support.test.filters.LargeTest;
32import android.support.test.runner.AndroidJUnit4;
33import android.view.LayoutInflater;
34import android.view.MotionEvent;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.ViewGroup.LayoutParams;
38import android.widget.TextView;
39
40import androidx.annotation.NonNull;
41import androidx.recyclerview.R;
42
43import org.junit.Test;
44import org.junit.runner.RunWith;
45
46import java.util.concurrent.CountDownLatch;
47import java.util.concurrent.TimeUnit;
48
49@LargeTest
50@RunWith(AndroidJUnit4.class)
51public 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