blob: 4cb050e246eeef04239834f67c7e0832c67e8d2d [file] [log] [blame]
Winson Chung94804152015-05-08 13:06:44 -07001/*
2 * Copyright (C) 2015 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 com.android.launcher3;
18
19import android.content.Context;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070020import android.graphics.Canvas;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070021import android.graphics.Rect;
Winson Chung94804152015-05-08 13:06:44 -070022import android.support.v7.widget.RecyclerView;
23import android.util.AttributeSet;
24import android.view.MotionEvent;
25import com.android.launcher3.util.Thunk;
26
Winson Chungb1777442015-06-16 13:35:04 -070027
Winson Chung94804152015-05-08 13:06:44 -070028/**
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070029 * A base {@link RecyclerView}, which does the following:
30 * <ul>
31 * <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
32 * <li> Enable fast scroller.
33 * </ul>
Winson Chung94804152015-05-08 13:06:44 -070034 */
Winson Chungb1777442015-06-16 13:35:04 -070035public abstract class BaseRecyclerView extends RecyclerView
Winson Chung94804152015-05-08 13:06:44 -070036 implements RecyclerView.OnItemTouchListener {
37
38 private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
39
40 /** Keeps the last known scrolling delta/velocity along y-axis. */
41 @Thunk int mDy = 0;
42 private float mDeltaThreshold;
Winson Chung94804152015-05-08 13:06:44 -070043
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070044 /**
Winson Chungb1777442015-06-16 13:35:04 -070045 * The current scroll state of the recycler view. We use this in onUpdateScrollbar()
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070046 * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
47 * that we can calculate what the scroll bar looks like, and where to jump to from the fast
48 * scroller.
49 */
50 public static class ScrollPositionState {
51 // The index of the first visible row
52 public int rowIndex;
53 // The offset of the first visible row
54 public int rowTopOffset;
Winsonc0880492015-08-21 11:16:27 -070055 // The adapter position of the first visible item
56 public int itemPos;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070057 }
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070058
Winson Chungb1777442015-06-16 13:35:04 -070059 protected BaseRecyclerViewFastScrollBar mScrollbar;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070060
61 private int mDownX;
62 private int mDownY;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070063 private int mLastY;
Winson Chungef7f8742015-06-04 17:18:17 -070064 protected Rect mBackgroundPadding = new Rect();
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070065
Winson Chung5f4e0fd2015-05-22 11:12:27 -070066 public BaseRecyclerView(Context context) {
Winson Chung94804152015-05-08 13:06:44 -070067 this(context, null);
68 }
69
Winson Chung5f4e0fd2015-05-22 11:12:27 -070070 public BaseRecyclerView(Context context, AttributeSet attrs) {
Winson Chung94804152015-05-08 13:06:44 -070071 this(context, attrs, 0);
72 }
73
Winson Chung5f4e0fd2015-05-22 11:12:27 -070074 public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
Winson Chung94804152015-05-08 13:06:44 -070075 super(context, attrs, defStyleAttr);
76 mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
Winson Chungb1777442015-06-16 13:35:04 -070077 mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources());
Winson Chung94804152015-05-08 13:06:44 -070078
79 ScrollListener listener = new ScrollListener();
80 setOnScrollListener(listener);
81 }
82
83 private class ScrollListener extends OnScrollListener {
84 public ScrollListener() {
85 // Do nothing
86 }
87
88 @Override
89 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
90 mDy = dy;
Winson Chungb1777442015-06-16 13:35:04 -070091
92 // TODO(winsonc): If we want to animate the section heads while scrolling, we can
93 // initiate that here if the recycler view scroll state is not
94 // RecyclerView.SCROLL_STATE_IDLE.
Winsond2eb49e2015-08-18 17:43:02 -070095
96 onUpdateScrollbar(dy);
Winson Chung94804152015-05-08 13:06:44 -070097 }
98 }
99
Winsond2eb49e2015-08-18 17:43:02 -0700100 public void reset() {
101 mScrollbar.reattachThumbToScroll();
102 }
103
Winson Chung94804152015-05-08 13:06:44 -0700104 @Override
105 protected void onFinishInflate() {
106 super.onFinishInflate();
107 addOnItemTouchListener(this);
108 }
109
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700110 /**
111 * We intercept the touch handling only to support fast scrolling when initiated from the
112 * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling.
113 */
Winson Chung94804152015-05-08 13:06:44 -0700114 @Override
115 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700116 return handleTouchEvent(ev);
Winson Chung94804152015-05-08 13:06:44 -0700117 }
118
119 @Override
120 public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700121 handleTouchEvent(ev);
122 }
123
124 /**
125 * Handles the touch event and determines whether to show the fast scroller (or updates it if
126 * it is already showing).
127 */
128 private boolean handleTouchEvent(MotionEvent ev) {
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700129 int action = ev.getAction();
130 int x = (int) ev.getX();
131 int y = (int) ev.getY();
132 switch (action) {
133 case MotionEvent.ACTION_DOWN:
134 // Keep track of the down positions
Hyunyoung Songec847282015-06-04 11:37:46 -0700135 mDownX = x;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700136 mDownY = mLastY = y;
137 if (shouldStopScroll(ev)) {
138 stopScroll();
139 }
Winson Chungb1777442015-06-16 13:35:04 -0700140 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700141 break;
142 case MotionEvent.ACTION_MOVE:
Winson Chungb1777442015-06-16 13:35:04 -0700143 mLastY = y;
144 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700145 break;
146 case MotionEvent.ACTION_UP:
147 case MotionEvent.ACTION_CANCEL:
Winson Chungb1777442015-06-16 13:35:04 -0700148 onFastScrollCompleted();
149 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700150 break;
151 }
Winsond2eb49e2015-08-18 17:43:02 -0700152 return mScrollbar.isDraggingThumb();
Winson Chung94804152015-05-08 13:06:44 -0700153 }
154
155 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
156 // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS
157 }
158
159 /**
Winson Chung94804152015-05-08 13:06:44 -0700160 * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
161 */
162 protected boolean shouldStopScroll(MotionEvent ev) {
163 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
164 if ((Math.abs(mDy) < mDeltaThreshold &&
165 getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
166 // now the touch events are being passed to the {@link WidgetCell} until the
167 // touch sequence goes over the touch slop.
168 return true;
169 }
170 }
171 return false;
172 }
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700173
Winson Chungef7f8742015-06-04 17:18:17 -0700174 public void updateBackgroundPadding(Rect padding) {
175 mBackgroundPadding.set(padding);
176 }
177
Winson Chungb1777442015-06-16 13:35:04 -0700178 public Rect getBackgroundPadding() {
179 return mBackgroundPadding;
180 }
181
182 /**
183 * Returns the scroll bar width when the user is scrolling.
184 */
185 public int getMaxScrollbarWidth() {
186 return mScrollbar.getThumbMaxWidth();
187 }
188
189 /**
Winsonc0880492015-08-21 11:16:27 -0700190 * Returns the visible height of the recycler view:
191 * VisibleHeight = View height - top padding - bottom padding
192 */
193 protected int getVisibleHeight() {
194 int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
195 return visibleHeight;
196 }
197
198 /**
Winson Chungb1777442015-06-16 13:35:04 -0700199 * Returns the available scroll height:
200 * AvailableScrollHeight = Total height of the all items - last page height
Winson Chungb1777442015-06-16 13:35:04 -0700201 */
Winsonc0880492015-08-21 11:16:27 -0700202 protected int getAvailableScrollHeight(int rowCount) {
203 int totalHeight = getPaddingTop() + getTop(rowCount) + getPaddingBottom();
204 int availableScrollHeight = totalHeight - getVisibleHeight();
Winson Chungb1777442015-06-16 13:35:04 -0700205 return availableScrollHeight;
206 }
207
208 /**
209 * Returns the available scroll bar height:
210 * AvailableScrollBarHeight = Total height of the visible view - thumb height
211 */
212 protected int getAvailableScrollBarHeight() {
Winsonc0880492015-08-21 11:16:27 -0700213 int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight();
Winson Chungb1777442015-06-16 13:35:04 -0700214 return availableScrollBarHeight;
215 }
216
217 /**
218 * Returns the track color (ignoring alpha), can be overridden by each subclass.
219 */
220 public int getFastScrollerTrackColor(int defaultTrackColor) {
221 return defaultTrackColor;
222 }
223
224 /**
225 * Returns the inactive thumb color, can be overridden by each subclass.
226 */
227 public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) {
228 return defaultInactiveThumbColor;
229 }
230
Winsonc0880492015-08-21 11:16:27 -0700231 /**
232 * Returns the scrollbar for this recycler view.
233 */
234 public BaseRecyclerViewFastScrollBar getScrollBar() {
235 return mScrollbar;
236 }
237
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700238 @Override
239 protected void dispatchDraw(Canvas canvas) {
240 super.dispatchDraw(canvas);
Winsond2eb49e2015-08-18 17:43:02 -0700241 onUpdateScrollbar(0);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700242 mScrollbar.draw(canvas);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700243 }
244
245 /**
Winson Chungb1777442015-06-16 13:35:04 -0700246 * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does
247 * this by mapping the available scroll area of the recycler view to the available space for the
248 * scroll bar.
249 *
250 * @param scrollPosState the current scroll position
251 * @param rowCount the number of rows, used to calculate the total scroll height (assumes that
252 * all rows are the same height)
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700253 */
Winson Chungb1777442015-06-16 13:35:04 -0700254 protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState,
Winsond2eb49e2015-08-18 17:43:02 -0700255 int rowCount) {
Winson Chungb1777442015-06-16 13:35:04 -0700256 // Only show the scrollbar if there is height to be scrolled
Winsond2eb49e2015-08-18 17:43:02 -0700257 int availableScrollBarHeight = getAvailableScrollBarHeight();
Winsonc0880492015-08-21 11:16:27 -0700258 int availableScrollHeight = getAvailableScrollHeight(rowCount);
Winson Chungb1777442015-06-16 13:35:04 -0700259 if (availableScrollHeight <= 0) {
Winsond2eb49e2015-08-18 17:43:02 -0700260 mScrollbar.setThumbOffset(-1, -1);
Winson Chungb1777442015-06-16 13:35:04 -0700261 return;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700262 }
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700263
Winson Chungb1777442015-06-16 13:35:04 -0700264 // Calculate the current scroll position, the scrollY of the recycler view accounts for the
265 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
266 // padding)
Peter Schillerc2a20ff2016-07-07 14:47:05 -0700267 int scrollY = Math.max(0, getScrollTop(scrollPosState));
Winson Chungb1777442015-06-16 13:35:04 -0700268 int scrollBarY = mBackgroundPadding.top +
269 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700270
Winson Chungb1777442015-06-16 13:35:04 -0700271 // Calculate the position and size of the scroll bar
272 int scrollBarX;
273 if (Utilities.isRtl(getResources())) {
274 scrollBarX = mBackgroundPadding.left;
275 } else {
Winsond2eb49e2015-08-18 17:43:02 -0700276 scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
Winson Chungb1777442015-06-16 13:35:04 -0700277 }
Winsond2eb49e2015-08-18 17:43:02 -0700278 mScrollbar.setThumbOffset(scrollBarX, scrollBarY);
Winson Chung4b9f9792015-06-12 18:02:52 -0700279 }
280
281 /**
Winsonc0880492015-08-21 11:16:27 -0700282 * @return whether fast scrolling is supported in the current state.
Winson646c2362015-09-03 11:46:11 -0700283 */
284 protected boolean supportsFastScrolling() {
285 return true;
286 }
287
288 /**
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700289 * Maps the touch (from 0..1) to the adapter position that should be visible.
290 * <p>Override in each subclass of this base class.
Winsonc0880492015-08-21 11:16:27 -0700291 *
292 * @return the scroll top of this recycler view.
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700293 */
Winsonc0880492015-08-21 11:16:27 -0700294 protected int getScrollTop(ScrollPositionState scrollPosState) {
295 return getPaddingTop() + getTop(scrollPosState.rowIndex) -
296 scrollPosState.rowTopOffset;
297 }
298
299 /**
300 * Returns information about the item that the recycler view is currently scrolled to.
301 */
302 protected abstract void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask);
303
304 /**
305 * Returns the top (or y position) of the row at the specified index.
306 */
307 protected abstract int getTop(int rowIndex);
308
309 /**
310 * Maps the touch (from 0..1) to the adapter position that should be visible.
311 * <p>Override in each subclass of this base class.
312 */
313 protected abstract String scrollToPositionAtProgress(float touchFraction);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700314
315 /**
316 * Updates the bounds for the scrollbar.
317 * <p>Override in each subclass of this base class.
318 */
Winsonc0880492015-08-21 11:16:27 -0700319 protected abstract void onUpdateScrollbar(int dy);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700320
321 /**
Winson Chungb1777442015-06-16 13:35:04 -0700322 * <p>Override in each subclass of this base class.
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700323 */
Winsonc0880492015-08-21 11:16:27 -0700324 protected void onFastScrollCompleted() {}
Winson Chung94804152015-05-08 13:06:44 -0700325}