blob: a207d9a12de85b26d80480175ce87282fe0479ce [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
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070019import android.animation.ObjectAnimator;
Winson Chung94804152015-05-08 13:06:44 -070020import android.content.Context;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070021import android.content.res.Resources;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.Paint;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
Winson Chung94804152015-05-08 13:06:44 -070027import android.support.v7.widget.RecyclerView;
28import android.util.AttributeSet;
29import android.view.MotionEvent;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070030import android.view.View;
31import android.view.ViewConfiguration;
32
Winson Chung94804152015-05-08 13:06:44 -070033import com.android.launcher3.util.Thunk;
34
35/**
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070036 * A base {@link RecyclerView}, which does the following:
37 * <ul>
38 * <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
39 * <li> Enable fast scroller.
40 * </ul>
Winson Chung94804152015-05-08 13:06:44 -070041 */
Winson Chung5f4e0fd2015-05-22 11:12:27 -070042public class BaseRecyclerView extends RecyclerView
Winson Chung94804152015-05-08 13:06:44 -070043 implements RecyclerView.OnItemTouchListener {
44
45 private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
46
47 /** Keeps the last known scrolling delta/velocity along y-axis. */
48 @Thunk int mDy = 0;
49 private float mDeltaThreshold;
Winson Chung94804152015-05-08 13:06:44 -070050
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070051 //
52 // Keeps track of variables required for the second function of this class: fast scroller.
53 //
54
55 private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
56
57 /**
58 * The current scroll state of the recycler view. We use this in updateVerticalScrollbarBounds()
59 * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
60 * that we can calculate what the scroll bar looks like, and where to jump to from the fast
61 * scroller.
62 */
63 public static class ScrollPositionState {
64 // The index of the first visible row
65 public int rowIndex;
66 // The offset of the first visible row
67 public int rowTopOffset;
68 // The height of a given row (they are currently all the same height)
69 public int rowHeight;
70 }
71 // Should be maintained inside overriden method #updateVerticalScrollbarBounds
72 public ScrollPositionState scrollPosState = new ScrollPositionState();
73 public Rect verticalScrollbarBounds = new Rect();
74
75 private boolean mDraggingFastScroller;
76
77 private Drawable mScrollbar;
78 private Drawable mFastScrollerBg;
79 private Rect mTmpFastScrollerInvalidateRect = new Rect();
80 private Rect mFastScrollerBounds = new Rect();
81
82 private String mFastScrollSectionName;
83 private Paint mFastScrollTextPaint;
84 private Rect mFastScrollTextBounds = new Rect();
85 private float mFastScrollAlpha;
86
87 private int mDownX;
88 private int mDownY;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070089 private int mLastY;
90 private int mScrollbarWidth;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070091 private int mScrollbarInset;
92 private Rect mBackgroundPadding = new Rect();
93
94
95
Winson Chung5f4e0fd2015-05-22 11:12:27 -070096 public BaseRecyclerView(Context context) {
Winson Chung94804152015-05-08 13:06:44 -070097 this(context, null);
98 }
99
Winson Chung5f4e0fd2015-05-22 11:12:27 -0700100 public BaseRecyclerView(Context context, AttributeSet attrs) {
Winson Chung94804152015-05-08 13:06:44 -0700101 this(context, attrs, 0);
102 }
103
Winson Chung5f4e0fd2015-05-22 11:12:27 -0700104 public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
Winson Chung94804152015-05-08 13:06:44 -0700105 super(context, attrs, defStyleAttr);
106 mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
107
108 ScrollListener listener = new ScrollListener();
109 setOnScrollListener(listener);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700110
111 Resources res = context.getResources();
112 int fastScrollerSize = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_popup_size);
113 mScrollbar = res.getDrawable(R.drawable.all_apps_scrollbar_thumb);
114 mFastScrollerBg = res.getDrawable(R.drawable.all_apps_fastscroll_bg);
115 mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
116 mFastScrollTextPaint = new Paint();
117 mFastScrollTextPaint.setColor(Color.WHITE);
118 mFastScrollTextPaint.setAntiAlias(true);
119 mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
120 R.dimen.all_apps_fast_scroll_text_size));
121 mScrollbarWidth = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_bar_width);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700122 mScrollbarInset =
123 res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_scrubber_touch_inset);
124 setFastScrollerAlpha(mFastScrollAlpha);
125 setOverScrollMode(View.OVER_SCROLL_NEVER);
Winson Chung94804152015-05-08 13:06:44 -0700126 }
127
128 private class ScrollListener extends OnScrollListener {
129 public ScrollListener() {
130 // Do nothing
131 }
132
133 @Override
134 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
135 mDy = dy;
Winson Chung94804152015-05-08 13:06:44 -0700136 }
137 }
138
Winson Chung94804152015-05-08 13:06:44 -0700139 @Override
140 protected void onFinishInflate() {
141 super.onFinishInflate();
142 addOnItemTouchListener(this);
143 }
144
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700145 /**
146 * We intercept the touch handling only to support fast scrolling when initiated from the
147 * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling.
148 */
Winson Chung94804152015-05-08 13:06:44 -0700149 @Override
150 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700151 return handleTouchEvent(ev);
Winson Chung94804152015-05-08 13:06:44 -0700152 }
153
154 @Override
155 public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700156 handleTouchEvent(ev);
157 }
158
159 /**
160 * Handles the touch event and determines whether to show the fast scroller (or updates it if
161 * it is already showing).
162 */
163 private boolean handleTouchEvent(MotionEvent ev) {
164 ViewConfiguration config = ViewConfiguration.get(getContext());
165
166 int action = ev.getAction();
167 int x = (int) ev.getX();
168 int y = (int) ev.getY();
169 switch (action) {
170 case MotionEvent.ACTION_DOWN:
171 // Keep track of the down positions
Hyunyoung Songec847282015-06-04 11:37:46 -0700172 mDownX = x;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700173 mDownY = mLastY = y;
174 if (shouldStopScroll(ev)) {
175 stopScroll();
176 }
177 break;
178 case MotionEvent.ACTION_MOVE:
179 // Check if we are scrolling
180 if (!mDraggingFastScroller && isPointNearScrollbar(mDownX, mDownY) &&
181 Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
182 getParent().requestDisallowInterceptTouchEvent(true);
183 mDraggingFastScroller = true;
184 animateFastScrollerVisibility(true);
185 }
186 if (mDraggingFastScroller) {
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700187 mLastY = y;
188
189 // Scroll to the right position, and update the section name
190 int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2);
191 int bottom = getHeight() - getPaddingBottom() -
192 (mFastScrollerBg.getBounds().height() / 2);
193 float boundedY = (float) Math.max(top, Math.min(bottom, y));
194 mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) /
195 (bottom - top));
196
197 // Combine the old and new fast scroller bounds to create the full invalidate
198 // rect
199 mTmpFastScrollerInvalidateRect.set(mFastScrollerBounds);
200 updateFastScrollerBounds();
201 mTmpFastScrollerInvalidateRect.union(mFastScrollerBounds);
202 invalidateFastScroller(mTmpFastScrollerInvalidateRect);
203 }
204 break;
205 case MotionEvent.ACTION_UP:
206 case MotionEvent.ACTION_CANCEL:
207 mDraggingFastScroller = false;
208 animateFastScrollerVisibility(false);
209 break;
210 }
211 return mDraggingFastScroller;
Winson Chung94804152015-05-08 13:06:44 -0700212 }
213
214 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
215 // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS
216 }
217
218 /**
Winson Chung94804152015-05-08 13:06:44 -0700219 * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
220 */
221 protected boolean shouldStopScroll(MotionEvent ev) {
222 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
223 if ((Math.abs(mDy) < mDeltaThreshold &&
224 getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
225 // now the touch events are being passed to the {@link WidgetCell} until the
226 // touch sequence goes over the touch slop.
227 return true;
228 }
229 }
230 return false;
231 }
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700232
233 @Override
234 protected void dispatchDraw(Canvas canvas) {
235 super.dispatchDraw(canvas);
236 drawVerticalScrubber(canvas);
237 drawFastScrollerPopup(canvas);
238 }
239
240 /**
241 * Draws the vertical scrollbar.
242 */
243 private void drawVerticalScrubber(Canvas canvas) {
244 updateVerticalScrollbarBounds();
245
246 // Draw the scroll bar
247 int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
248 canvas.translate(verticalScrollbarBounds.left, verticalScrollbarBounds.top);
249 mScrollbar.setBounds(0, 0, mScrollbarWidth, verticalScrollbarBounds.height());
250 mScrollbar.draw(canvas);
251 canvas.restoreToCount(restoreCount);
252 }
253
254 /**
255 * Draws the fast scroller popup.
256 */
257 private void drawFastScrollerPopup(Canvas canvas) {
258 if (mFastScrollAlpha > 0f && mFastScrollSectionName != null && !mFastScrollSectionName.isEmpty()) {
259 // Draw the fast scroller popup
260 int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
261 canvas.translate(mFastScrollerBounds.left, mFastScrollerBounds.top);
262 mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
263 mFastScrollerBg.draw(canvas);
264 mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
265 mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
266 mFastScrollSectionName.length(), mFastScrollTextBounds);
267 float textWidth = mFastScrollTextPaint.measureText(mFastScrollSectionName);
268 canvas.drawText(mFastScrollSectionName,
269 (mFastScrollerBounds.width() - textWidth) / 2,
270 mFastScrollerBounds.height() -
271 (mFastScrollerBounds.height() - mFastScrollTextBounds.height()) / 2,
272 mFastScrollTextPaint);
273 canvas.restoreToCount(restoreCount);
274 }
275 }
276
277 /**
278 * Returns the scroll bar width.
279 */
280 public int getScrollbarWidth() {
281 return mScrollbarWidth;
282 }
283
284 /**
285 * Sets the fast scroller alpha.
286 */
287 public void setFastScrollerAlpha(float alpha) {
288 mFastScrollAlpha = alpha;
289 invalidateFastScroller(mFastScrollerBounds);
290 }
291
292 /**
293 * Maps the touch (from 0..1) to the adapter position that should be visible.
294 * <p>Override in each subclass of this base class.
295 */
296 public String scrollToPositionAtProgress(float touchFraction) {
297 return null;
298 }
299
300 /**
301 * Updates the bounds for the scrollbar.
302 * <p>Override in each subclass of this base class.
303 */
304 public void updateVerticalScrollbarBounds() {};
305
306 /**
307 * Animates the visibility of the fast scroller popup.
308 */
309 private void animateFastScrollerVisibility(boolean visible) {
310 ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
311 anim.setDuration(visible ? 200 : 150);
312 anim.start();
313 }
314
315 /**
316 * Invalidates the fast scroller popup.
317 */
318 protected void invalidateFastScroller(Rect bounds) {
319 invalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
320 }
321
322 /**
323 * Returns whether a given point is near the scrollbar.
324 */
325 private boolean isPointNearScrollbar(int x, int y) {
326 // Check if we are scrolling
327 updateVerticalScrollbarBounds();
328 verticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset);
329 return verticalScrollbarBounds.contains(x, y);
330 }
331
332 /**
333 * Updates the bounds for the fast scroller.
334 */
335 private void updateFastScrollerBounds() {
336 if (mFastScrollAlpha > 0f && !mFastScrollSectionName.isEmpty()) {
337 int x;
338 int y;
339
340 // Calculate the position for the fast scroller popup
341 Rect bgBounds = mFastScrollerBg.getBounds();
342 if (Utilities.isRtl(getResources())) {
343 x = mBackgroundPadding.left + getScrollBarSize();
344 } else {
345 x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
346 }
347 y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
348 y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
349 bgBounds.height()));
350 mFastScrollerBounds.set(x, y, x + bgBounds.width(), y + bgBounds.height());
351 } else {
352 mFastScrollerBounds.setEmpty();
353 }
354 }
Winson Chung94804152015-05-08 13:06:44 -0700355}