blob: 140c28c0c41ad3b7ce1e6d358ace0e648f055b13 [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
Winson Chungfbc5b182015-06-12 14:18:55 -070019import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070021import android.animation.ObjectAnimator;
Winson Chung94804152015-05-08 13:06:44 -070022import android.content.Context;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070023import android.content.res.Resources;
24import android.graphics.Canvas;
25import android.graphics.Color;
26import android.graphics.Paint;
27import android.graphics.Rect;
28import android.graphics.drawable.Drawable;
Winson Chung94804152015-05-08 13:06:44 -070029import android.support.v7.widget.RecyclerView;
30import android.util.AttributeSet;
31import android.view.MotionEvent;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070032import android.view.View;
33import android.view.ViewConfiguration;
34
Winson Chung94804152015-05-08 13:06:44 -070035import com.android.launcher3.util.Thunk;
36
37/**
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070038 * A base {@link RecyclerView}, which does the following:
39 * <ul>
40 * <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
41 * <li> Enable fast scroller.
42 * </ul>
Winson Chung94804152015-05-08 13:06:44 -070043 */
Winson Chung5f4e0fd2015-05-22 11:12:27 -070044public class BaseRecyclerView extends RecyclerView
Winson Chung94804152015-05-08 13:06:44 -070045 implements RecyclerView.OnItemTouchListener {
46
47 private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
48
49 /** Keeps the last known scrolling delta/velocity along y-axis. */
50 @Thunk int mDy = 0;
51 private float mDeltaThreshold;
Winson Chung94804152015-05-08 13:06:44 -070052
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070053 //
54 // Keeps track of variables required for the second function of this class: fast scroller.
55 //
56
57 private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
58
59 /**
60 * The current scroll state of the recycler view. We use this in updateVerticalScrollbarBounds()
61 * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
62 * that we can calculate what the scroll bar looks like, and where to jump to from the fast
63 * scroller.
64 */
65 public static class ScrollPositionState {
66 // The index of the first visible row
67 public int rowIndex;
68 // The offset of the first visible row
69 public int rowTopOffset;
70 // The height of a given row (they are currently all the same height)
71 public int rowHeight;
72 }
73 // Should be maintained inside overriden method #updateVerticalScrollbarBounds
74 public ScrollPositionState scrollPosState = new ScrollPositionState();
75 public Rect verticalScrollbarBounds = new Rect();
76
77 private boolean mDraggingFastScroller;
78
79 private Drawable mScrollbar;
80 private Drawable mFastScrollerBg;
81 private Rect mTmpFastScrollerInvalidateRect = new Rect();
82 private Rect mFastScrollerBounds = new Rect();
83
84 private String mFastScrollSectionName;
85 private Paint mFastScrollTextPaint;
86 private Rect mFastScrollTextBounds = new Rect();
87 private float mFastScrollAlpha;
88
89 private int mDownX;
90 private int mDownY;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070091 private int mLastY;
92 private int mScrollbarWidth;
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070093 private int mScrollbarInset;
Winson Chungef7f8742015-06-04 17:18:17 -070094 protected Rect mBackgroundPadding = new Rect();
Hyunyoung Songac5f6af2015-05-29 12:00:44 -070095
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
Winson Chungef7f8742015-06-04 17:18:17 -0700233 public void updateBackgroundPadding(Rect padding) {
234 mBackgroundPadding.set(padding);
235 }
236
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700237 @Override
238 protected void dispatchDraw(Canvas canvas) {
239 super.dispatchDraw(canvas);
240 drawVerticalScrubber(canvas);
241 drawFastScrollerPopup(canvas);
242 }
243
244 /**
245 * Draws the vertical scrollbar.
246 */
247 private void drawVerticalScrubber(Canvas canvas) {
248 updateVerticalScrollbarBounds();
249
250 // Draw the scroll bar
251 int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
252 canvas.translate(verticalScrollbarBounds.left, verticalScrollbarBounds.top);
253 mScrollbar.setBounds(0, 0, mScrollbarWidth, verticalScrollbarBounds.height());
254 mScrollbar.draw(canvas);
255 canvas.restoreToCount(restoreCount);
256 }
257
258 /**
259 * Draws the fast scroller popup.
260 */
261 private void drawFastScrollerPopup(Canvas canvas) {
262 if (mFastScrollAlpha > 0f && mFastScrollSectionName != null && !mFastScrollSectionName.isEmpty()) {
263 // Draw the fast scroller popup
264 int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
265 canvas.translate(mFastScrollerBounds.left, mFastScrollerBounds.top);
266 mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
267 mFastScrollerBg.draw(canvas);
268 mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
269 mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
270 mFastScrollSectionName.length(), mFastScrollTextBounds);
271 float textWidth = mFastScrollTextPaint.measureText(mFastScrollSectionName);
272 canvas.drawText(mFastScrollSectionName,
273 (mFastScrollerBounds.width() - textWidth) / 2,
274 mFastScrollerBounds.height() -
275 (mFastScrollerBounds.height() - mFastScrollTextBounds.height()) / 2,
276 mFastScrollTextPaint);
277 canvas.restoreToCount(restoreCount);
278 }
279 }
280
281 /**
282 * Returns the scroll bar width.
283 */
284 public int getScrollbarWidth() {
285 return mScrollbarWidth;
286 }
287
288 /**
289 * Sets the fast scroller alpha.
290 */
291 public void setFastScrollerAlpha(float alpha) {
292 mFastScrollAlpha = alpha;
293 invalidateFastScroller(mFastScrollerBounds);
294 }
295
296 /**
Winson Chung4b9f9792015-06-12 18:02:52 -0700297 * Returns the fast scroller alpha.
298 */
299 public float getFastScrollerAlpha() {
300 return mFastScrollAlpha;
301 }
302
303 /**
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700304 * Maps the touch (from 0..1) to the adapter position that should be visible.
305 * <p>Override in each subclass of this base class.
306 */
307 public String scrollToPositionAtProgress(float touchFraction) {
308 return null;
309 }
310
311 /**
312 * Updates the bounds for the scrollbar.
313 * <p>Override in each subclass of this base class.
314 */
315 public void updateVerticalScrollbarBounds() {};
316
317 /**
318 * Animates the visibility of the fast scroller popup.
319 */
Winson Chungfbc5b182015-06-12 14:18:55 -0700320 private void animateFastScrollerVisibility(final boolean visible) {
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700321 ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
322 anim.setDuration(visible ? 200 : 150);
Winson Chungfbc5b182015-06-12 14:18:55 -0700323 anim.addListener(new AnimatorListenerAdapter() {
324 @Override
325 public void onAnimationStart(Animator animation) {
326 if (visible) {
327 onFastScrollingStart();
328 }
329 }
330
331 @Override
332 public void onAnimationEnd(Animator animation) {
333 if (!visible) {
334 onFastScrollingEnd();
335 }
336 }
337 });
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700338 anim.start();
339 }
340
341 /**
Winson Chungfbc5b182015-06-12 14:18:55 -0700342 * To be overridden by subclasses.
343 */
344 protected void onFastScrollingStart() {}
345
346 /**
347 * To be overridden by subclasses.
348 */
349 protected void onFastScrollingEnd() {}
350
351 /**
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700352 * Invalidates the fast scroller popup.
353 */
354 protected void invalidateFastScroller(Rect bounds) {
355 invalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
356 }
357
358 /**
359 * Returns whether a given point is near the scrollbar.
360 */
361 private boolean isPointNearScrollbar(int x, int y) {
362 // Check if we are scrolling
363 updateVerticalScrollbarBounds();
364 verticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset);
365 return verticalScrollbarBounds.contains(x, y);
366 }
367
368 /**
369 * Updates the bounds for the fast scroller.
370 */
371 private void updateFastScrollerBounds() {
372 if (mFastScrollAlpha > 0f && !mFastScrollSectionName.isEmpty()) {
373 int x;
374 int y;
375
376 // Calculate the position for the fast scroller popup
377 Rect bgBounds = mFastScrollerBg.getBounds();
378 if (Utilities.isRtl(getResources())) {
Winson Chungef7f8742015-06-04 17:18:17 -0700379 x = mBackgroundPadding.left + (2 * getScrollbarWidth());
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700380 } else {
Winson Chungef7f8742015-06-04 17:18:17 -0700381 x = getWidth() - mBackgroundPadding.right - (2 * getScrollbarWidth()) -
382 bgBounds.width();
Hyunyoung Songac5f6af2015-05-29 12:00:44 -0700383 }
384 y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
385 y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
386 bgBounds.height()));
387 mFastScrollerBounds.set(x, y, x + bgBounds.width(), y + bgBounds.height());
388 } else {
389 mFastScrollerBounds.setEmpty();
390 }
391 }
Winson Chung94804152015-05-08 13:06:44 -0700392}