3 | * Copyright (C) 2016 Tim Malseed
4 | * Copyright (C) 2015 The Android Open Source Project
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | *
18 | */
19 |
20 | package com.jaredrummler.fastscrollrecyclerview;
21 |
22 | import android.content.Context;
23 | import android.content.res.TypedArray;
24 | import android.graphics.Canvas;
25 | import android.graphics.Rect;
26 | import android.support.annotation.ColorInt;
27 | import android.support.annotation.NonNull;
28 | import android.support.v7.widget.GridLayoutManager;
29 | import android.support.v7.widget.LinearLayoutManager;
30 | import android.support.v7.widget.RecyclerView;
31 | import android.util.AttributeSet;
32 | import android.view.MotionEvent;
33 | import android.view.View;
34 |
35 | /**
36 | * A base {@link RecyclerView}, which does the following:
37 | *
38 | *
39 | * - NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
40 | *
- Enable fast scroller.
41 | *
42 | */
43 | public class FastScrollRecyclerView extends RecyclerView implements RecyclerView.OnItemTouchListener {
44 |
45 | private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
46 | private static final int DEFAULT_HIDE_DELAY = 1000;
47 |
48 | private final ScrollPositionState scrollPositionState = new ScrollPositionState();
49 | private final Rect backgroundPadding = new Rect();
50 | /*package*/ FastScrollBar fastScrollBar;
51 | /*package*/ boolean fastScrollAlwaysEnabled;
52 | private float deltaThreshold;
53 | private int hideDelay;
54 | /*package*/ int lastDy; // Keeps the last known scrolling delta/velocity along y-axis.
55 | private int downX;
56 | private int downY;
57 | private int lastY;
58 |
59 | final Runnable hide = new Runnable() {
60 |
61 | @Override public void run() {
62 | if (!fastScrollBar.isDraggingThumb()) {
63 | fastScrollBar.animateScrollbar(false);
64 | }
65 | }
66 | };
67 |
68 | public FastScrollRecyclerView(Context context) {
69 | this(context, null);
70 | }
71 |
72 | public FastScrollRecyclerView(Context context, AttributeSet attrs) {
73 | this(context, attrs, 0);
74 | }
75 |
76 | public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
77 | super(context, attrs, defStyleAttr);
78 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FastScrollRecyclerView);
79 | fastScrollAlwaysEnabled = ta.getBoolean(R.styleable.FastScrollRecyclerView_fastScrollAlwaysEnabled, false);
80 | hideDelay = ta.getInt(R.styleable.FastScrollRecyclerView_fastScrollHideDelay, DEFAULT_HIDE_DELAY);
81 | ta.recycle();
82 | deltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
83 | fastScrollBar = new FastScrollBar(this, attrs);
84 | fastScrollBar.setDetachThumbOnFastScroll();
85 | addOnScrollListener(new OnScrollListener() {
86 |
87 | @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
88 | if (fastScrollAlwaysEnabled) return;
89 | switch (newState) {
90 | case SCROLL_STATE_DRAGGING:
91 | removeCallbacks(hide);
92 | fastScrollBar.animateScrollbar(true);
93 | break;
94 | case SCROLL_STATE_IDLE:
95 | hideScrollBar();
96 | break;
97 | }
98 | }
99 |
100 | @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
101 | lastDy = dy;
102 | onUpdateScrollbar(dy);
103 | }
104 | });
105 | }
106 |
107 | public void reset() {
108 | fastScrollBar.reattachThumbToScroll();
109 | }
110 |
111 | @Override protected void onFinishInflate() {
112 | super.onFinishInflate();
113 | addOnItemTouchListener(this);
114 | }
115 |
116 | /**
117 | * We intercept the touch handling only to support fast scrolling when initiated from the
118 | * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling.
119 | */
120 | @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
121 | return handleTouchEvent(ev);
122 | }
123 |
124 | @Override public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
125 | handleTouchEvent(ev);
126 | }
127 |
128 | /**
129 | * Handles the touch event and determines whether to show the fast scroller (or updates it if
130 | * it is already showing).
131 | */
132 | private boolean handleTouchEvent(MotionEvent ev) {
133 | int action = ev.getAction();
134 | int x = (int) ev.getX();
135 | int y = (int) ev.getY();
136 | switch (action) {
137 | case MotionEvent.ACTION_DOWN:
138 | // Keep track of the down positions
139 | downX = x;
140 | downY = lastY = y;
141 | if (shouldStopScroll(ev)) {
142 | stopScroll();
143 | }
144 | fastScrollBar.handleTouchEvent(ev, downX, downY, lastY);
145 | break;
146 | case MotionEvent.ACTION_MOVE:
147 | lastY = y;
148 | fastScrollBar.handleTouchEvent(ev, downX, downY, lastY);
149 | break;
150 | case MotionEvent.ACTION_UP:
151 | case MotionEvent.ACTION_CANCEL:
152 | onFastScrollCompleted();
153 | fastScrollBar.handleTouchEvent(ev, downX, downY, lastY);
154 | break;
155 | }
156 | return fastScrollBar.isDraggingThumb();
157 | }
158 |
159 | @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
160 | // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS
161 | }
162 |
163 | /**
164 | * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
165 | */
166 | protected boolean shouldStopScroll(MotionEvent ev) {
167 | if (ev.getAction() == MotionEvent.ACTION_DOWN) {
168 | if ((Math.abs(lastDy) < deltaThreshold && getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
169 | // now the touch events are being passed to the {@link WidgetCell} until the
170 | // touch sequence goes over the touch slop.
171 | return true;
172 | }
173 | }
174 | return false;
175 | }
176 |
177 | public void updateBackgroundPadding(Rect padding) {
178 | backgroundPadding.set(padding);
179 | }
180 |
181 | public Rect getBackgroundPadding() {
182 | return backgroundPadding;
183 | }
184 |
185 | /**
186 | * Returns the scroll bar width when the user is scrolling.
187 | */
188 | public int getMaxScrollbarWidth() {
189 | return fastScrollBar.getThumbMaxWidth();
190 | }
191 |
192 | /**
193 | * Returns the available scroll height:
194 | * AvailableScrollHeight = Total height of the all items - last page height
195 | *
196 | * This assumes that all rows are the same height.
197 | */
198 | protected int getAvailableScrollHeight(int rowCount, int rowHeight) {
199 | int visibleHeight = getHeight() - backgroundPadding.top - backgroundPadding.bottom;
200 | int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom();
201 | return scrollHeight - visibleHeight;
202 | }
203 |
204 | /**
205 | * Returns the available scroll bar height:
206 | * AvailableScrollBarHeight = Total height of the visible view - thumb height
207 | */
208 | protected int getAvailableScrollBarHeight() {
209 | int visibleHeight = getHeight() - backgroundPadding.top - backgroundPadding.bottom;
210 | return visibleHeight - fastScrollBar.getThumbHeight();
211 | }
212 |
213 | public boolean isFastScrollAlwaysEnabled() {
214 | return fastScrollAlwaysEnabled;
215 | }
216 |
217 | protected void hideScrollBar() {
218 | if (!fastScrollAlwaysEnabled) {
219 | removeCallbacks(hide);
220 | postDelayed(hide, hideDelay);
221 | }
222 | }
223 |
224 | public void setThumbActiveColor(@ColorInt int color) {
225 | fastScrollBar.setThumbActiveColor(color);
226 | }
227 |
228 | public void setTrackInactiveColor(@ColorInt int color) {
229 | fastScrollBar.setThumbInactiveColor(color);
230 | }
231 |
232 | public void setPopupBackgroundColor(@ColorInt int color) {
233 | fastScrollBar.setPopupBackgroundColor(color);
234 | }
235 |
236 | public void setPopupTextColor(@ColorInt int color) {
237 | fastScrollBar.setPopupTextColor(color);
238 | }
239 |
240 | public FastScrollBar getFastScrollBar() {
241 | return fastScrollBar;
242 | }
243 |
244 | @Override
245 | public void draw(Canvas canvas) {
246 | super.draw(canvas);
247 |
248 | // Draw the ScrollBar AFTER the ItemDecorations are drawn over
249 | onUpdateScrollbar(0);
250 | fastScrollBar.draw(canvas);
251 | }
252 |
253 | /**
254 | * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does
255 | * this by mapping the available scroll area of the recycler view to the available space for the
256 | * scroll bar.
257 | *
258 | * @param scrollPosState
259 | * the current scroll position
260 | * @param rowCount
261 | * the number of rows, used to calculate the total scroll height (assumes that
262 | * all rows are the same height)
263 | */
264 | protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, int rowCount) {
265 | // Only show the scrollbar if there is height to be scrolled
266 | int availableScrollBarHeight = getAvailableScrollBarHeight();
267 | int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight);
268 | if (availableScrollHeight <= 0) {
269 | fastScrollBar.setThumbOffset(-1, -1);
270 | return;
271 | }
272 |
273 | // Calculate the current scroll position, the scrollY of the recycler view accounts for the
274 | // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
275 | // padding)
276 | int scrollY = getPaddingTop() +
277 | Math.round(((scrollPosState.rowIndex - scrollPosState.rowTopOffset) * scrollPosState.rowHeight));
278 | int scrollBarY =
279 | backgroundPadding.top + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
280 |
281 | // Calculate the position and size of the scroll bar
282 | int scrollBarX;
283 | if (Utilities.isRtl(getResources())) {
284 | scrollBarX = backgroundPadding.left;
285 | } else {
286 | scrollBarX = getWidth() - backgroundPadding.right - fastScrollBar.getThumbWidth();
287 | }
288 | fastScrollBar.setThumbOffset(scrollBarX, scrollBarY);
289 | }
290 |
291 | /**
292 | * Maps the touch (from 0..1) to the adapter position that should be visible.
293 | *
294 | * Override in each subclass of this base class.
295 | */
296 | public String scrollToPositionAtProgress(float touchFraction) {
297 | int itemCount = getAdapter().getItemCount();
298 | if (itemCount == 0) {
299 | return "";
300 | }
301 | int spanCount = 1;
302 | int rowCount = itemCount;
303 | if (getLayoutManager() instanceof GridLayoutManager) {
304 | spanCount = ((GridLayoutManager) getLayoutManager()).getSpanCount();
305 | rowCount = (int) Math.ceil((double) rowCount / spanCount);
306 | }
307 |
308 | // Stop the scroller if it is scrolling
309 | stopScroll();
310 |
311 | getCurScrollState(scrollPositionState);
312 |
313 | float itemPos = itemCount * touchFraction;
314 |
315 | int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPositionState.rowHeight);
316 |
317 | //The exact position of our desired item
318 | int exactItemPos = (int) (availableScrollHeight * touchFraction);
319 |
320 | //Scroll to the desired item. The offset used here is kind of hard to explain.
321 | //If the position we wish to scroll to is, say, position 10.5, we scroll to position 10,
322 | //and then offset by 0.5 * rowHeight. This is how we achieve smooth scrolling.
323 | LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
324 | layoutManager.scrollToPositionWithOffset(spanCount * exactItemPos / scrollPositionState.rowHeight,
325 | -(exactItemPos % scrollPositionState.rowHeight));
326 |
327 | if (!(getAdapter() instanceof SectionedAdapter)) {
328 | return "";
329 | }
330 |
331 | int posInt = (int) ((touchFraction == 1) ? itemPos - 1 : itemPos);
332 |
333 | SectionedAdapter sectionedAdapter = (SectionedAdapter) getAdapter();
334 | return sectionedAdapter.getSectionName(posInt);
335 | }
336 |
337 | /**
338 | * Updates the bounds for the scrollbar.
339 | *
340 | * Override in each subclass of this base class.
341 | */
342 | public void onUpdateScrollbar(int dy) {
343 | int rowCount = getAdapter().getItemCount();
344 | if (getLayoutManager() instanceof GridLayoutManager) {
345 | int spanCount = ((GridLayoutManager) getLayoutManager()).getSpanCount();
346 | rowCount = (int) Math.ceil((double) rowCount / spanCount);
347 | }
348 | // Skip early if, there are no items.
349 | if (rowCount == 0) {
350 | fastScrollBar.setThumbOffset(-1, -1);
351 | return;
352 | }
353 |
354 | // Skip early if, there no child laid out in the container.
355 | getCurScrollState(scrollPositionState);
356 | if (scrollPositionState.rowIndex < 0) {
357 | fastScrollBar.setThumbOffset(-1, -1);
358 | return;
359 | }
360 |
361 | synchronizeScrollBarThumbOffsetToViewScroll(scrollPositionState, rowCount);
362 | }
363 |
364 | /**
365 | * Override in each subclass of this base class.
366 | */
367 | public void onFastScrollCompleted() {
368 | }
369 |
370 | /**
371 | * Returns information about the item that the recycler view is currently scrolled to.
372 | */
373 | protected void getCurScrollState(ScrollPositionState stateOut) {
374 | stateOut.rowIndex = -1;
375 | stateOut.rowTopOffset = -1;
376 | stateOut.rowHeight = -1;
377 |
378 | // Return early if there are no items
379 | int rowCount = getAdapter().getItemCount();
380 | if (rowCount == 0) {
381 | return;
382 | }
383 |
384 | View child = getChildAt(0);
385 | if (child == null) {
386 | return;
387 | }
388 |
389 | stateOut.rowIndex = getChildPosition(child);
390 | if (getLayoutManager() instanceof GridLayoutManager) {
391 | stateOut.rowIndex = stateOut.rowIndex / ((GridLayoutManager) getLayoutManager()).getSpanCount();
392 | }
393 | stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child) / (float) child.getHeight();
394 | stateOut.rowHeight = calculateRowHeight(child.getHeight());
395 | }
396 |
397 | /**
398 | * Calculates the row height based on the average of the visible children, to handle scrolling
399 | * through children with different heights gracefully
400 | */
401 | protected int calculateRowHeight(int fallbackHeight) {
402 | LayoutManager layoutManager = getLayoutManager();
403 |
404 | if (layoutManager instanceof LinearLayoutManager) {
405 | final int firstVisiblePosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
406 | final int lastVisiblePosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
407 |
408 | if (lastVisiblePosition > firstVisiblePosition) {
409 | final int height = getHeight();
410 | final int paddingTop = getPaddingTop();
411 | final int paddingBottom = getPaddingBottom();
412 |
413 | // How many rows are visible, like 10.5f for 10 rows completely and one halfway visible
414 | float visibleRows = 0f;
415 |
416 | for (int position = firstVisiblePosition; position <= lastVisiblePosition; position++) {
417 | ViewHolder viewHolder = findViewHolderForLayoutPosition(position);
418 | if (viewHolder == null || viewHolder.itemView == null) {
419 | continue;
420 | }
421 |
422 | final View itemView = viewHolder.itemView;
423 | final int itemHeight = itemView.getHeight();
424 | if (itemHeight == 0) {
425 | continue;
426 | }
427 |
428 | // Finds how much of the itemView is actually visible.
429 | // This allows smooth changes of the scrollbar thumb height
430 | final int visibleHeight = itemHeight
431 | - Math.max(0, paddingBottom - layoutManager.getDecoratedTop(itemView)) // How much is cut at the top
432 | - Math.max(0, paddingBottom + layoutManager.getDecoratedBottom(itemView) - height); // How much is cut at the bottom
433 |
434 | visibleRows += visibleHeight / (float) itemHeight;
435 | }
436 |
437 | return Math.round((height - (paddingTop + paddingBottom)) / visibleRows);
438 | }
439 | }
440 |
441 | return fallbackHeight;
442 | }
443 |
444 | /**
445 | * Iterface to implement in your {@link RecyclerView.Adapter} to show a popup next to the scroller
446 | */
447 | public interface SectionedAdapter {
448 |
449 | /**
450 | * @param position
451 | * the item position
452 | * @return the section name for this item
453 | */
454 | @NonNull String getSectionName(int position);
455 | }
456 |
457 | /**
458 | * The current scroll state of the recycler view. We use this in onUpdateScrollbar()
459 | * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
460 | * that we can calculate what the scroll bar looks like, and where to jump to from the fast
461 | * scroller.
462 | */
463 | public static class ScrollPositionState {
464 |
465 | // The index of the first visible row
466 | public int rowIndex;
467 | // The offset of the first visible row, in percentage of the height
468 | public float rowTopOffset;
469 | // The height of a given row (they are currently all the same height)
470 | public int rowHeight;
471 | }
472 |
473 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/jaredrummler/fastscrollrecyclerview/Utilities.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Jared Rummler
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 |
18 | package com.jaredrummler.fastscrollrecyclerview;
19 |
20 | import android.content.res.Resources;
21 | import android.os.Build;
22 | import android.view.View;
23 |
24 | final class Utilities {
25 |
26 | static boolean isRtl(Resources res) {
27 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
28 | res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable-ldrtl/fastscroll_popup_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
20 |
21 |
24 |
28 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/fastscroll_popup_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
21 |
22 |
25 |
29 |
--------------------------------------------------------------------------------
/library/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/library/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
21 | #009688
22 | #009688
23 |
24 |
25 |
--------------------------------------------------------------------------------
/library/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 | 24dp
21 | 48dp
22 | 0dp
23 | 9dp
24 | 72dp
25 | -24dp
26 |
27 |
28 |
--------------------------------------------------------------------------------
/library/src/test/java/com/jaredrummler/fastscrollrecyclerview/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Jared Rummler
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 |
18 | package com.jaredrummler.fastscrollrecyclerview;
19 |
20 | import org.junit.Test;
21 |
22 | import static org.junit.Assert.*;
23 |
24 | /**
25 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
26 | */
27 | public class ExampleUnitTest {
28 |
29 | @Test
30 | public void addition_isCorrect() throws Exception {
31 | assertEquals(4, 2 + 2);
32 | }
33 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Jared Rummler
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 |
18 | include ':demo', ':library'
19 |
--------------------------------------------------------------------------------