blob: a6801696a6cfb8d340d8924a54085c50f1757af9 [file] [log] [blame]
Winson Chungb1777442015-06-16 13:35:04 -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 */
16package com.android.launcher3;
17
Winson Chungb1777442015-06-16 13:35:04 -070018import android.animation.AnimatorSet;
19import android.animation.ArgbEvaluator;
20import android.animation.ObjectAnimator;
21import android.animation.ValueAnimator;
22import android.content.res.Resources;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.Paint;
Winson67795952015-08-20 12:23:52 -070026import android.graphics.Path;
Winson Chungb1777442015-06-16 13:35:04 -070027import android.graphics.Point;
28import android.graphics.Rect;
29import android.view.MotionEvent;
Winsonc0880492015-08-21 11:16:27 -070030import android.view.VelocityTracker;
Winson Chungb1777442015-06-16 13:35:04 -070031import android.view.ViewConfiguration;
32
Sunny Goyalb713ad42015-06-24 11:45:32 -070033import com.android.launcher3.util.Thunk;
34
Winson Chungb1777442015-06-16 13:35:04 -070035/**
36 * The track and scrollbar that shows when you scroll the list.
37 */
38public class BaseRecyclerViewFastScrollBar {
39
40 public interface FastScrollFocusableView {
Winsonc0880492015-08-21 11:16:27 -070041 void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated);
Winson Chungb1777442015-06-16 13:35:04 -070042 }
43
44 private final static int MAX_TRACK_ALPHA = 30;
45 private final static int SCROLL_BAR_VIS_DURATION = 150;
46
Sunny Goyalb713ad42015-06-24 11:45:32 -070047 @Thunk BaseRecyclerView mRv;
Winson Chungb1777442015-06-16 13:35:04 -070048 private BaseRecyclerViewFastScrollPopup mPopup;
49
50 private AnimatorSet mScrollbarAnimator;
51
52 private int mThumbInactiveColor;
53 private int mThumbActiveColor;
Sunny Goyalb713ad42015-06-24 11:45:32 -070054 @Thunk Point mThumbOffset = new Point(-1, -1);
55 @Thunk Paint mThumbPaint;
Winson Chungb1777442015-06-16 13:35:04 -070056 private int mThumbMinWidth;
57 private int mThumbMaxWidth;
Sunny Goyalb713ad42015-06-24 11:45:32 -070058 @Thunk int mThumbWidth;
59 @Thunk int mThumbHeight;
Winson67795952015-08-20 12:23:52 -070060 private int mThumbCurvature;
61 private Path mThumbPath = new Path();
62 private Paint mTrackPaint;
63 private int mTrackWidth;
Winsond2eb49e2015-08-18 17:43:02 -070064 private float mLastTouchY;
Winson Chungb1777442015-06-16 13:35:04 -070065 // The inset is the buffer around which a point will still register as a click on the scrollbar
66 private int mTouchInset;
67 private boolean mIsDragging;
Winsond2eb49e2015-08-18 17:43:02 -070068 private boolean mIsThumbDetached;
69 private boolean mCanThumbDetach;
Winsonec4845b2015-08-27 10:19:48 -070070 private boolean mIgnoreDragGesture;
Winson Chungb1777442015-06-16 13:35:04 -070071
72 // This is the offset from the top of the scrollbar when the user first starts touching. To
73 // prevent jumping, this offset is applied as the user scrolls.
74 private int mTouchOffset;
75
76 private Rect mInvalidateRect = new Rect();
77 private Rect mTmpRect = new Rect();
78
79 public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
80 mRv = rv;
81 mPopup = new BaseRecyclerViewFastScrollPopup(rv, res);
82 mTrackPaint = new Paint();
83 mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
Winson67795952015-08-20 12:23:52 -070084 mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
Winson Chungb1777442015-06-16 13:35:04 -070085 mThumbInactiveColor = rv.getFastScrollerThumbInactiveColor(
86 res.getColor(R.color.container_fastscroll_thumb_inactive_color));
87 mThumbActiveColor = res.getColor(R.color.container_fastscroll_thumb_active_color);
88 mThumbPaint = new Paint();
Winson67795952015-08-20 12:23:52 -070089 mThumbPaint.setAntiAlias(true);
Winson Chungb1777442015-06-16 13:35:04 -070090 mThumbPaint.setColor(mThumbInactiveColor);
Winson67795952015-08-20 12:23:52 -070091 mThumbPaint.setStyle(Paint.Style.FILL);
Winson Chungb1777442015-06-16 13:35:04 -070092 mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
93 mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
94 mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
Winson67795952015-08-20 12:23:52 -070095 mThumbCurvature = mThumbMaxWidth - mThumbMinWidth;
Winson Chungb1777442015-06-16 13:35:04 -070096 mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
97 }
98
Winsond2eb49e2015-08-18 17:43:02 -070099 public void setDetachThumbOnFastScroll() {
100 mCanThumbDetach = true;
101 }
102
103 public void reattachThumbToScroll() {
104 mIsThumbDetached = false;
105 }
106
107 public void setThumbOffset(int x, int y) {
Winson Chungb1777442015-06-16 13:35:04 -0700108 if (mThumbOffset.x == x && mThumbOffset.y == y) {
109 return;
110 }
Winson67795952015-08-20 12:23:52 -0700111 mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
112 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
Winson Chungb1777442015-06-16 13:35:04 -0700113 mThumbOffset.set(x, y);
Winson67795952015-08-20 12:23:52 -0700114 updateThumbPath();
115 mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
116 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
Winson Chungb1777442015-06-16 13:35:04 -0700117 mRv.invalidate(mInvalidateRect);
118 }
119
Winsond2eb49e2015-08-18 17:43:02 -0700120 public Point getThumbOffset() {
121 return mThumbOffset;
122 }
123
Winson67795952015-08-20 12:23:52 -0700124 // Setter/getter for the thumb bar width for animations
Winsond2eb49e2015-08-18 17:43:02 -0700125 public void setThumbWidth(int width) {
Winson67795952015-08-20 12:23:52 -0700126 mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
127 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
Winson Chungb1777442015-06-16 13:35:04 -0700128 mThumbWidth = width;
Winson67795952015-08-20 12:23:52 -0700129 updateThumbPath();
130 mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
131 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
Winson Chungb1777442015-06-16 13:35:04 -0700132 mRv.invalidate(mInvalidateRect);
133 }
134
Winsond2eb49e2015-08-18 17:43:02 -0700135 public int getThumbWidth() {
Winson Chungb1777442015-06-16 13:35:04 -0700136 return mThumbWidth;
137 }
138
Winson67795952015-08-20 12:23:52 -0700139 // Setter/getter for the track bar width for animations
140 public void setTrackWidth(int width) {
141 mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
142 mRv.getHeight());
143 mTrackWidth = width;
144 updateThumbPath();
145 mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
146 mRv.getHeight());
Winson Chungb1777442015-06-16 13:35:04 -0700147 mRv.invalidate(mInvalidateRect);
148 }
149
Winson67795952015-08-20 12:23:52 -0700150 public int getTrackWidth() {
151 return mTrackWidth;
Winson Chungb1777442015-06-16 13:35:04 -0700152 }
153
154 public int getThumbHeight() {
155 return mThumbHeight;
156 }
157
158 public int getThumbMaxWidth() {
159 return mThumbMaxWidth;
160 }
161
Winsond2eb49e2015-08-18 17:43:02 -0700162 public float getLastTouchY() {
163 return mLastTouchY;
164 }
165
166 public boolean isDraggingThumb() {
Winson Chungb1777442015-06-16 13:35:04 -0700167 return mIsDragging;
168 }
169
Winsond2eb49e2015-08-18 17:43:02 -0700170 public boolean isThumbDetached() {
171 return mIsThumbDetached;
172 }
173
Winson Chungb1777442015-06-16 13:35:04 -0700174 /**
175 * Handles the touch event and determines whether to show the fast scroller (or updates it if
176 * it is already showing).
177 */
178 public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) {
179 ViewConfiguration config = ViewConfiguration.get(mRv.getContext());
180
181 int action = ev.getAction();
182 int y = (int) ev.getY();
183 switch (action) {
184 case MotionEvent.ACTION_DOWN:
Winsonec4845b2015-08-27 10:19:48 -0700185 if (isNearThumb(downX, downY)) {
Winson Chungb1777442015-06-16 13:35:04 -0700186 mTouchOffset = downY - mThumbOffset.y;
187 }
188 break;
189 case MotionEvent.ACTION_MOVE:
Winsonec4845b2015-08-27 10:19:48 -0700190 // Check if we should start scrolling, but ignore this fastscroll gesture if we have
191 // exceeded some fixed movement
192 mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop();
Winson646c2362015-09-03 11:46:11 -0700193 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
194 isNearThumb(downX, lastY) &&
Winson Chungb1777442015-06-16 13:35:04 -0700195 Math.abs(y - downY) > config.getScaledTouchSlop()) {
196 mRv.getParent().requestDisallowInterceptTouchEvent(true);
197 mIsDragging = true;
Winsond2eb49e2015-08-18 17:43:02 -0700198 if (mCanThumbDetach) {
199 mIsThumbDetached = true;
200 }
Winson Chungb1777442015-06-16 13:35:04 -0700201 mTouchOffset += (lastY - downY);
202 mPopup.animateVisibility(true);
Winsonc0880492015-08-21 11:16:27 -0700203 showActiveScrollbar(true);
Winson Chungb1777442015-06-16 13:35:04 -0700204 }
205 if (mIsDragging) {
206 // Update the fastscroller section name at this touch position
207 int top = mRv.getBackgroundPadding().top;
208 int bottom = mRv.getHeight() - mRv.getBackgroundPadding().bottom - mThumbHeight;
209 float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset));
210 String sectionName = mRv.scrollToPositionAtProgress((boundedY - top) /
211 (bottom - top));
212 mPopup.setSectionName(sectionName);
213 mPopup.animateVisibility(!sectionName.isEmpty());
Winsonc0880492015-08-21 11:16:27 -0700214 mRv.invalidate(mPopup.updateFastScrollerBounds(lastY));
Winsond2eb49e2015-08-18 17:43:02 -0700215 mLastTouchY = boundedY;
Winson Chungb1777442015-06-16 13:35:04 -0700216 }
217 break;
218 case MotionEvent.ACTION_UP:
219 case MotionEvent.ACTION_CANCEL:
Winson Chungb1777442015-06-16 13:35:04 -0700220 mTouchOffset = 0;
Winsond2eb49e2015-08-18 17:43:02 -0700221 mLastTouchY = 0;
Winsonec4845b2015-08-27 10:19:48 -0700222 mIgnoreDragGesture = false;
Winson Chung4c7fc622015-06-24 16:59:31 -0700223 if (mIsDragging) {
224 mIsDragging = false;
225 mPopup.animateVisibility(false);
Winsonc0880492015-08-21 11:16:27 -0700226 showActiveScrollbar(false);
Winson Chung4c7fc622015-06-24 16:59:31 -0700227 }
Winson Chungb1777442015-06-16 13:35:04 -0700228 break;
229 }
230 }
231
232 public void draw(Canvas canvas) {
233 if (mThumbOffset.x < 0 || mThumbOffset.y < 0) {
234 return;
235 }
236
237 // Draw the scroll bar track and thumb
238 if (mTrackPaint.getAlpha() > 0) {
239 canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight(), mTrackPaint);
240 }
Winson67795952015-08-20 12:23:52 -0700241 canvas.drawPath(mThumbPath, mThumbPaint);
Winson Chungb1777442015-06-16 13:35:04 -0700242
243 // Draw the popup
244 mPopup.draw(canvas);
245 }
246
247 /**
248 * Animates the width and color of the scrollbar.
249 */
Winsonc0880492015-08-21 11:16:27 -0700250 private void showActiveScrollbar(boolean isScrolling) {
Winson Chungb1777442015-06-16 13:35:04 -0700251 if (mScrollbarAnimator != null) {
252 mScrollbarAnimator.cancel();
253 }
Winson67795952015-08-20 12:23:52 -0700254
255 mScrollbarAnimator = new AnimatorSet();
256 ObjectAnimator trackWidthAnim = ObjectAnimator.ofInt(this, "trackWidth",
257 isScrolling ? mThumbMaxWidth : mThumbMinWidth);
Winsond2eb49e2015-08-18 17:43:02 -0700258 ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth",
Winson Chungb1777442015-06-16 13:35:04 -0700259 isScrolling ? mThumbMaxWidth : mThumbMinWidth);
Winson67795952015-08-20 12:23:52 -0700260 mScrollbarAnimator.playTogether(trackWidthAnim, thumbWidthAnim);
261 if (mThumbActiveColor != mThumbInactiveColor) {
262 ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
263 mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor);
264 colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
265 @Override
266 public void onAnimationUpdate(ValueAnimator animator) {
267 mThumbPaint.setColor((Integer) animator.getAnimatedValue());
268 mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
269 mThumbOffset.y + mThumbHeight);
270 }
271 });
272 mScrollbarAnimator.play(colorAnimation);
273 }
Winson Chungb1777442015-06-16 13:35:04 -0700274 mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
275 mScrollbarAnimator.start();
276 }
277
278 /**
Winson67795952015-08-20 12:23:52 -0700279 * Updates the path for the thumb drawable.
280 */
281 private void updateThumbPath() {
282 mThumbCurvature = mThumbMaxWidth - mThumbWidth;
283 mThumbPath.reset();
284 mThumbPath.moveTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y); // tr
285 mThumbPath.lineTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight); // br
286 mThumbPath.lineTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight); // bl
287 mThumbPath.cubicTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight,
288 mThumbOffset.x - mThumbCurvature, mThumbOffset.y + mThumbHeight / 2,
289 mThumbOffset.x, mThumbOffset.y); // bl2tl
290 mThumbPath.close();
291 }
292
293 /**
Winson Chungb1777442015-06-16 13:35:04 -0700294 * Returns whether the specified points are near the scroll bar bounds.
295 */
Winsonec4845b2015-08-27 10:19:48 -0700296 private boolean isNearThumb(int x, int y) {
Winson Chungb1777442015-06-16 13:35:04 -0700297 mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
298 mThumbOffset.y + mThumbHeight);
299 mTmpRect.inset(mTouchInset, mTouchInset);
300 return mTmpRect.contains(x, y);
301 }
302}