blob: 3d71632ce752771e6d3c6154b6e039d926eebd23 [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;
30import android.view.ViewConfiguration;
31
Sunny Goyalb713ad42015-06-24 11:45:32 -070032import com.android.launcher3.util.Thunk;
33
Winson Chungb1777442015-06-16 13:35:04 -070034/**
35 * The track and scrollbar that shows when you scroll the list.
36 */
37public class BaseRecyclerViewFastScrollBar {
38
39 public interface FastScrollFocusableView {
Winsonc0880492015-08-21 11:16:27 -070040 void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated);
Winson Chungb1777442015-06-16 13:35:04 -070041 }
42
43 private final static int MAX_TRACK_ALPHA = 30;
44 private final static int SCROLL_BAR_VIS_DURATION = 150;
45
Sunny Goyalb713ad42015-06-24 11:45:32 -070046 @Thunk BaseRecyclerView mRv;
Winson Chungb1777442015-06-16 13:35:04 -070047 private BaseRecyclerViewFastScrollPopup mPopup;
48
49 private AnimatorSet mScrollbarAnimator;
50
51 private int mThumbInactiveColor;
52 private int mThumbActiveColor;
Sunny Goyalb713ad42015-06-24 11:45:32 -070053 @Thunk Point mThumbOffset = new Point(-1, -1);
54 @Thunk Paint mThumbPaint;
Winson Chungb1777442015-06-16 13:35:04 -070055 private int mThumbMinWidth;
56 private int mThumbMaxWidth;
Sunny Goyalb713ad42015-06-24 11:45:32 -070057 @Thunk int mThumbWidth;
58 @Thunk int mThumbHeight;
Winson67795952015-08-20 12:23:52 -070059 private int mThumbCurvature;
60 private Path mThumbPath = new Path();
61 private Paint mTrackPaint;
62 private int mTrackWidth;
Winsond2eb49e2015-08-18 17:43:02 -070063 private float mLastTouchY;
Winson Chungb1777442015-06-16 13:35:04 -070064 // The inset is the buffer around which a point will still register as a click on the scrollbar
65 private int mTouchInset;
66 private boolean mIsDragging;
Winsond2eb49e2015-08-18 17:43:02 -070067 private boolean mIsThumbDetached;
68 private boolean mCanThumbDetach;
Winsonec4845b2015-08-27 10:19:48 -070069 private boolean mIgnoreDragGesture;
Winson Chungb1777442015-06-16 13:35:04 -070070
71 // This is the offset from the top of the scrollbar when the user first starts touching. To
72 // prevent jumping, this offset is applied as the user scrolls.
73 private int mTouchOffset;
74
75 private Rect mInvalidateRect = new Rect();
76 private Rect mTmpRect = new Rect();
77
78 public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
79 mRv = rv;
80 mPopup = new BaseRecyclerViewFastScrollPopup(rv, res);
81 mTrackPaint = new Paint();
82 mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
Winson67795952015-08-20 12:23:52 -070083 mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
Andrew Sappersteinabef55a2016-06-19 12:49:00 -070084 mThumbActiveColor = mThumbInactiveColor = Utilities.getColorAccent(rv.getContext());
Winson Chungb1777442015-06-16 13:35:04 -070085 mThumbPaint = new Paint();
Winson67795952015-08-20 12:23:52 -070086 mThumbPaint.setAntiAlias(true);
Winson Chungb1777442015-06-16 13:35:04 -070087 mThumbPaint.setColor(mThumbInactiveColor);
Winson67795952015-08-20 12:23:52 -070088 mThumbPaint.setStyle(Paint.Style.FILL);
Winson Chungb1777442015-06-16 13:35:04 -070089 mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
90 mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
91 mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
Winson67795952015-08-20 12:23:52 -070092 mThumbCurvature = mThumbMaxWidth - mThumbMinWidth;
Winson Chungb1777442015-06-16 13:35:04 -070093 mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
94 }
95
Winsond2eb49e2015-08-18 17:43:02 -070096 public void setDetachThumbOnFastScroll() {
97 mCanThumbDetach = true;
98 }
99
100 public void reattachThumbToScroll() {
101 mIsThumbDetached = false;
102 }
103
104 public void setThumbOffset(int x, int y) {
Winson Chungb1777442015-06-16 13:35:04 -0700105 if (mThumbOffset.x == x && mThumbOffset.y == y) {
106 return;
107 }
Winson67795952015-08-20 12:23:52 -0700108 mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
109 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
Winson Chungb1777442015-06-16 13:35:04 -0700110 mThumbOffset.set(x, y);
Winson67795952015-08-20 12:23:52 -0700111 updateThumbPath();
112 mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
113 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
Winson Chungb1777442015-06-16 13:35:04 -0700114 mRv.invalidate(mInvalidateRect);
115 }
116
Winsond2eb49e2015-08-18 17:43:02 -0700117 public Point getThumbOffset() {
118 return mThumbOffset;
119 }
120
Winson67795952015-08-20 12:23:52 -0700121 // Setter/getter for the thumb bar width for animations
Winsond2eb49e2015-08-18 17:43:02 -0700122 public void setThumbWidth(int width) {
Winson67795952015-08-20 12:23:52 -0700123 mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
124 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
Winson Chungb1777442015-06-16 13:35:04 -0700125 mThumbWidth = width;
Winson67795952015-08-20 12:23:52 -0700126 updateThumbPath();
127 mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
128 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
Winson Chungb1777442015-06-16 13:35:04 -0700129 mRv.invalidate(mInvalidateRect);
130 }
131
Winsond2eb49e2015-08-18 17:43:02 -0700132 public int getThumbWidth() {
Winson Chungb1777442015-06-16 13:35:04 -0700133 return mThumbWidth;
134 }
135
Winson67795952015-08-20 12:23:52 -0700136 // Setter/getter for the track bar width for animations
137 public void setTrackWidth(int width) {
138 mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
Hyunyoung Song4ebc3d02016-08-05 10:59:17 -0700139 mRv.getVisibleHeight());
Winson67795952015-08-20 12:23:52 -0700140 mTrackWidth = width;
141 updateThumbPath();
142 mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
Hyunyoung Song4ebc3d02016-08-05 10:59:17 -0700143 mRv.getVisibleHeight());
Winson Chungb1777442015-06-16 13:35:04 -0700144 mRv.invalidate(mInvalidateRect);
145 }
146
Winson67795952015-08-20 12:23:52 -0700147 public int getTrackWidth() {
148 return mTrackWidth;
Winson Chungb1777442015-06-16 13:35:04 -0700149 }
150
151 public int getThumbHeight() {
152 return mThumbHeight;
153 }
154
155 public int getThumbMaxWidth() {
156 return mThumbMaxWidth;
157 }
158
Winsond2eb49e2015-08-18 17:43:02 -0700159 public boolean isDraggingThumb() {
Winson Chungb1777442015-06-16 13:35:04 -0700160 return mIsDragging;
161 }
162
Winsond2eb49e2015-08-18 17:43:02 -0700163 public boolean isThumbDetached() {
164 return mIsThumbDetached;
165 }
166
Winson Chungb1777442015-06-16 13:35:04 -0700167 /**
168 * Handles the touch event and determines whether to show the fast scroller (or updates it if
169 * it is already showing).
170 */
171 public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) {
172 ViewConfiguration config = ViewConfiguration.get(mRv.getContext());
173
174 int action = ev.getAction();
175 int y = (int) ev.getY();
176 switch (action) {
177 case MotionEvent.ACTION_DOWN:
Winsonec4845b2015-08-27 10:19:48 -0700178 if (isNearThumb(downX, downY)) {
Winson Chungb1777442015-06-16 13:35:04 -0700179 mTouchOffset = downY - mThumbOffset.y;
180 }
181 break;
182 case MotionEvent.ACTION_MOVE:
Winsonec4845b2015-08-27 10:19:48 -0700183 // Check if we should start scrolling, but ignore this fastscroll gesture if we have
184 // exceeded some fixed movement
185 mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop();
Winson646c2362015-09-03 11:46:11 -0700186 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
187 isNearThumb(downX, lastY) &&
Winson Chungb1777442015-06-16 13:35:04 -0700188 Math.abs(y - downY) > config.getScaledTouchSlop()) {
189 mRv.getParent().requestDisallowInterceptTouchEvent(true);
190 mIsDragging = true;
Winsond2eb49e2015-08-18 17:43:02 -0700191 if (mCanThumbDetach) {
192 mIsThumbDetached = true;
193 }
Winson Chungb1777442015-06-16 13:35:04 -0700194 mTouchOffset += (lastY - downY);
195 mPopup.animateVisibility(true);
Winsonc0880492015-08-21 11:16:27 -0700196 showActiveScrollbar(true);
Winson Chungb1777442015-06-16 13:35:04 -0700197 }
198 if (mIsDragging) {
199 // Update the fastscroller section name at this touch position
200 int top = mRv.getBackgroundPadding().top;
Hyunyoung Song4ebc3d02016-08-05 10:59:17 -0700201 int bottom = top + mRv.getVisibleHeight() - mThumbHeight;
Winson Chungb1777442015-06-16 13:35:04 -0700202 float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset));
203 String sectionName = mRv.scrollToPositionAtProgress((boundedY - top) /
204 (bottom - top));
205 mPopup.setSectionName(sectionName);
206 mPopup.animateVisibility(!sectionName.isEmpty());
Winsonc0880492015-08-21 11:16:27 -0700207 mRv.invalidate(mPopup.updateFastScrollerBounds(lastY));
Winsond2eb49e2015-08-18 17:43:02 -0700208 mLastTouchY = boundedY;
Winsone8eaa9c2016-07-12 14:10:50 -0700209 setThumbOffset(mRv.getScrollBarX(), (int) mLastTouchY);
Winson Chungb1777442015-06-16 13:35:04 -0700210 }
211 break;
212 case MotionEvent.ACTION_UP:
213 case MotionEvent.ACTION_CANCEL:
Winson Chungb1777442015-06-16 13:35:04 -0700214 mTouchOffset = 0;
Winsond2eb49e2015-08-18 17:43:02 -0700215 mLastTouchY = 0;
Winsonec4845b2015-08-27 10:19:48 -0700216 mIgnoreDragGesture = false;
Winson Chung4c7fc622015-06-24 16:59:31 -0700217 if (mIsDragging) {
218 mIsDragging = false;
219 mPopup.animateVisibility(false);
Winsonc0880492015-08-21 11:16:27 -0700220 showActiveScrollbar(false);
Winson Chung4c7fc622015-06-24 16:59:31 -0700221 }
Winson Chungb1777442015-06-16 13:35:04 -0700222 break;
223 }
224 }
225
226 public void draw(Canvas canvas) {
227 if (mThumbOffset.x < 0 || mThumbOffset.y < 0) {
228 return;
229 }
230
231 // Draw the scroll bar track and thumb
232 if (mTrackPaint.getAlpha() > 0) {
Hyunyoung Song4ebc3d02016-08-05 10:59:17 -0700233 canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth,
234 mRv.getVisibleHeight(), mTrackPaint);
Winson Chungb1777442015-06-16 13:35:04 -0700235 }
Winson67795952015-08-20 12:23:52 -0700236 canvas.drawPath(mThumbPath, mThumbPaint);
Winson Chungb1777442015-06-16 13:35:04 -0700237
238 // Draw the popup
239 mPopup.draw(canvas);
240 }
241
242 /**
243 * Animates the width and color of the scrollbar.
244 */
Winsonc0880492015-08-21 11:16:27 -0700245 private void showActiveScrollbar(boolean isScrolling) {
Winson Chungb1777442015-06-16 13:35:04 -0700246 if (mScrollbarAnimator != null) {
247 mScrollbarAnimator.cancel();
248 }
Winson67795952015-08-20 12:23:52 -0700249
250 mScrollbarAnimator = new AnimatorSet();
251 ObjectAnimator trackWidthAnim = ObjectAnimator.ofInt(this, "trackWidth",
252 isScrolling ? mThumbMaxWidth : mThumbMinWidth);
Winsond2eb49e2015-08-18 17:43:02 -0700253 ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth",
Winson Chungb1777442015-06-16 13:35:04 -0700254 isScrolling ? mThumbMaxWidth : mThumbMinWidth);
Winson67795952015-08-20 12:23:52 -0700255 mScrollbarAnimator.playTogether(trackWidthAnim, thumbWidthAnim);
256 if (mThumbActiveColor != mThumbInactiveColor) {
257 ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
258 mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor);
259 colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
260 @Override
261 public void onAnimationUpdate(ValueAnimator animator) {
262 mThumbPaint.setColor((Integer) animator.getAnimatedValue());
263 mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
264 mThumbOffset.y + mThumbHeight);
265 }
266 });
267 mScrollbarAnimator.play(colorAnimation);
268 }
Winson Chungb1777442015-06-16 13:35:04 -0700269 mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
270 mScrollbarAnimator.start();
271 }
272
273 /**
Winson67795952015-08-20 12:23:52 -0700274 * Updates the path for the thumb drawable.
275 */
276 private void updateThumbPath() {
277 mThumbCurvature = mThumbMaxWidth - mThumbWidth;
278 mThumbPath.reset();
279 mThumbPath.moveTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y); // tr
280 mThumbPath.lineTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight); // br
281 mThumbPath.lineTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight); // bl
282 mThumbPath.cubicTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight,
283 mThumbOffset.x - mThumbCurvature, mThumbOffset.y + mThumbHeight / 2,
284 mThumbOffset.x, mThumbOffset.y); // bl2tl
285 mThumbPath.close();
286 }
287
288 /**
Winson Chungb1777442015-06-16 13:35:04 -0700289 * Returns whether the specified points are near the scroll bar bounds.
290 */
Hyunyoung Songf4cbb142016-06-10 12:00:02 -0700291 public boolean isNearThumb(int x, int y) {
Winson Chungb1777442015-06-16 13:35:04 -0700292 mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
293 mThumbOffset.y + mThumbHeight);
294 mTmpRect.inset(mTouchInset, mTouchInset);
295 return mTmpRect.contains(x, y);
296 }
297}