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