blob: 081c4f5022d0c6d0f6a6f4a2e1a806f7d073a94c [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;
Winson Chungef7f8742015-06-04 17:18:17 -070092 protected Rect mBackgroundPadding = new Rect();
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070093
Winson Chung5f4e0fd2015-05-22 11:12:27 -070094 public BaseRecyclerView(Context context) {
Winson Chung94804152015-05-08 13:06:44 -070095 this(context, null);
96 }
97
Winson Chung5f4e0fd2015-05-22 11:12:27 -070098 public BaseRecyclerView(Context context, AttributeSet attrs) {
Winson Chung94804152015-05-08 13:06:44 -070099 this(context, attrs, 0);
100 }
101
Winson Chung5f4e0fd2015-05-22 11:12:27 -0700102 public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
Winson Chung94804152015-05-08 13:06:44 -0700103 super(context, attrs, defStyleAttr);
104 mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
105
106 ScrollListener listener = new ScrollListener();
107 setOnScrollListener(listener);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700108
109 Resources res = context.getResources();
110 int fastScrollerSize = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_popup_size);
111 mScrollbar = res.getDrawable(R.drawable.all_apps_scrollbar_thumb);
112 mFastScrollerBg = res.getDrawable(R.drawable.all_apps_fastscroll_bg);
113 mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
114 mFastScrollTextPaint = new Paint();
115 mFastScrollTextPaint.setColor(Color.WHITE);
116 mFastScrollTextPaint.setAntiAlias(true);
117 mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
118 R.dimen.all_apps_fast_scroll_text_size));
119 mScrollbarWidth = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_bar_width);
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700120 mScrollbarInset =
121 res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_scrubber_touch_inset);
122 setFastScrollerAlpha(mFastScrollAlpha);
123 setOverScrollMode(View.OVER_SCROLL_NEVER);
Winson Chung94804152015-05-08 13:06:44 -0700124 }
125
126 private class ScrollListener extends OnScrollListener {
127 public ScrollListener() {
128 // Do nothing
129 }
130
131 @Override
132 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
133 mDy = dy;
Winson Chung94804152015-05-08 13:06:44 -0700134 }
135 }
136
Winson Chung94804152015-05-08 13:06:44 -0700137 @Override
138 protected void onFinishInflate() {
139 super.onFinishInflate();
140 addOnItemTouchListener(this);
141 }
142
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700143 /**
144 * We intercept the touch handling only to support fast scrolling when initiated from the
145 * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling.
146 */
Winson Chung94804152015-05-08 13:06:44 -0700147 @Override
148 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700149 return handleTouchEvent(ev);
Winson Chung94804152015-05-08 13:06:44 -0700150 }
151
152 @Override
153 public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700154 handleTouchEvent(ev);
155 }
156
157 /**
158 * Handles the touch event and determines whether to show the fast scroller (or updates it if
159 * it is already showing).
160 */
161 private boolean handleTouchEvent(MotionEvent ev) {
162 ViewConfiguration config = ViewConfiguration.get(getContext());
163
164 int action = ev.getAction();
165 int x = (int) ev.getX();
166 int y = (int) ev.getY();
167 switch (action) {
168 case MotionEvent.ACTION_DOWN:
169 // Keep track of the down positions
Hyunyoung Songec847282015-06-04 11:37:46 -0700170 mDownX = x;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700171 mDownY = mLastY = y;
172 if (shouldStopScroll(ev)) {
173 stopScroll();
174 }
175 break;
176 case MotionEvent.ACTION_MOVE:
177 // Check if we are scrolling
178 if (!mDraggingFastScroller && isPointNearScrollbar(mDownX, mDownY) &&
179 Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
180 getParent().requestDisallowInterceptTouchEvent(true);
181 mDraggingFastScroller = true;
182 animateFastScrollerVisibility(true);
183 }
184 if (mDraggingFastScroller) {
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700185 mLastY = y;
186
187 // Scroll to the right position, and update the section name
188 int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2);
189 int bottom = getHeight() - getPaddingBottom() -
190 (mFastScrollerBg.getBounds().height() / 2);
191 float boundedY = (float) Math.max(top, Math.min(bottom, y));
192 mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) /
193 (bottom - top));
194
195 // Combine the old and new fast scroller bounds to create the full invalidate
196 // rect
197 mTmpFastScrollerInvalidateRect.set(mFastScrollerBounds);
198 updateFastScrollerBounds();
199 mTmpFastScrollerInvalidateRect.union(mFastScrollerBounds);
200 invalidateFastScroller(mTmpFastScrollerInvalidateRect);
201 }
202 break;
203 case MotionEvent.ACTION_UP:
204 case MotionEvent.ACTION_CANCEL:
205 mDraggingFastScroller = false;
206 animateFastScrollerVisibility(false);
207 break;
208 }
209 return mDraggingFastScroller;
Winson Chung94804152015-05-08 13:06:44 -0700210 }
211
212 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
213 // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS
214 }
215
216 /**
Winson Chung94804152015-05-08 13:06:44 -0700217 * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
218 */
219 protected boolean shouldStopScroll(MotionEvent ev) {
220 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
221 if ((Math.abs(mDy) < mDeltaThreshold &&
222 getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
223 // now the touch events are being passed to the {@link WidgetCell} until the
224 // touch sequence goes over the touch slop.
225 return true;
226 }
227 }
228 return false;
229 }
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700230
Winson Chungef7f8742015-06-04 17:18:17 -0700231 public void updateBackgroundPadding(Rect padding) {
232 mBackgroundPadding.set(padding);
233 }
234
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700235 @Override
236 protected void dispatchDraw(Canvas canvas) {
237 super.dispatchDraw(canvas);
238 drawVerticalScrubber(canvas);
239 drawFastScrollerPopup(canvas);
240 }
241
242 /**
243 * Draws the vertical scrollbar.
244 */
245 private void drawVerticalScrubber(Canvas canvas) {
246 updateVerticalScrollbarBounds();
247
248 // Draw the scroll bar
249 int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
250 canvas.translate(verticalScrollbarBounds.left, verticalScrollbarBounds.top);
251 mScrollbar.setBounds(0, 0, mScrollbarWidth, verticalScrollbarBounds.height());
252 mScrollbar.draw(canvas);
253 canvas.restoreToCount(restoreCount);
254 }
255
256 /**
257 * Draws the fast scroller popup.
258 */
259 private void drawFastScrollerPopup(Canvas canvas) {
260 if (mFastScrollAlpha > 0f && mFastScrollSectionName != null && !mFastScrollSectionName.isEmpty()) {
261 // Draw the fast scroller popup
262 int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
263 canvas.translate(mFastScrollerBounds.left, mFastScrollerBounds.top);
264 mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
265 mFastScrollerBg.draw(canvas);
266 mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
267 mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
268 mFastScrollSectionName.length(), mFastScrollTextBounds);
269 float textWidth = mFastScrollTextPaint.measureText(mFastScrollSectionName);
270 canvas.drawText(mFastScrollSectionName,
271 (mFastScrollerBounds.width() - textWidth) / 2,
272 mFastScrollerBounds.height() -
273 (mFastScrollerBounds.height() - mFastScrollTextBounds.height()) / 2,
274 mFastScrollTextPaint);
275 canvas.restoreToCount(restoreCount);
276 }
277 }
278
279 /**
280 * Returns the scroll bar width.
281 */
282 public int getScrollbarWidth() {
283 return mScrollbarWidth;
284 }
285
286 /**
287 * Sets the fast scroller alpha.
288 */
289 public void setFastScrollerAlpha(float alpha) {
290 mFastScrollAlpha = alpha;
291 invalidateFastScroller(mFastScrollerBounds);
292 }
293
294 /**
295 * Maps the touch (from 0..1) to the adapter position that should be visible.
296 * <p>Override in each subclass of this base class.
297 */
298 public String scrollToPositionAtProgress(float touchFraction) {
299 return null;
300 }
301
302 /**
303 * Updates the bounds for the scrollbar.
304 * <p>Override in each subclass of this base class.
305 */
306 public void updateVerticalScrollbarBounds() {};
307
308 /**
309 * Animates the visibility of the fast scroller popup.
310 */
311 private void animateFastScrollerVisibility(boolean visible) {
312 ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
313 anim.setDuration(visible ? 200 : 150);
314 anim.start();
315 }
316
317 /**
318 * Invalidates the fast scroller popup.
319 */
320 protected void invalidateFastScroller(Rect bounds) {
321 invalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
322 }
323
324 /**
325 * Returns whether a given point is near the scrollbar.
326 */
327 private boolean isPointNearScrollbar(int x, int y) {
328 // Check if we are scrolling
329 updateVerticalScrollbarBounds();
330 verticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset);
331 return verticalScrollbarBounds.contains(x, y);
332 }
333
334 /**
335 * Updates the bounds for the fast scroller.
336 */
337 private void updateFastScrollerBounds() {
338 if (mFastScrollAlpha > 0f && !mFastScrollSectionName.isEmpty()) {
339 int x;
340 int y;
341
342 // Calculate the position for the fast scroller popup
343 Rect bgBounds = mFastScrollerBg.getBounds();
344 if (Utilities.isRtl(getResources())) {
Winson Chungef7f8742015-06-04 17:18:17 -0700345 x = mBackgroundPadding.left + (2 * getScrollbarWidth());
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700346 } else {
Winson Chungef7f8742015-06-04 17:18:17 -0700347 x = getWidth() - mBackgroundPadding.right - (2 * getScrollbarWidth()) -
348 bgBounds.width();
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700349 }
350 y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
351 y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
352 bgBounds.height()));
353 mFastScrollerBounds.set(x, y, x + bgBounds.width(), y + bgBounds.height());
354 } else {
355 mFastScrollerBounds.setEmpty();
356 }
357 }
Winson Chung94804152015-05-08 13:06:44 -0700358}