1 /*
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 package com.android.launcher3;
17 
18 import android.animation.AnimatorSet;
19 import android.animation.ArgbEvaluator;
20 import android.animation.ObjectAnimator;
21 import android.animation.ValueAnimator;
22 import android.content.res.Resources;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.Path;
27 import android.graphics.Point;
28 import android.graphics.Rect;
29 import android.view.MotionEvent;
30 import android.view.VelocityTracker;
31 import android.view.ViewConfiguration;
32 
33 import com.android.launcher3.util.Thunk;
34 
35 /**
36  * The track and scrollbar that shows when you scroll the list.
37  */
38 public class BaseRecyclerViewFastScrollBar {
39 
40     public interface FastScrollFocusableView {
setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated)41         void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated);
42     }
43 
44     private final static int MAX_TRACK_ALPHA = 30;
45     private final static int SCROLL_BAR_VIS_DURATION = 150;
46 
47     @Thunk BaseRecyclerView mRv;
48     private BaseRecyclerViewFastScrollPopup mPopup;
49 
50     private AnimatorSet mScrollbarAnimator;
51 
52     private int mThumbInactiveColor;
53     private int mThumbActiveColor;
54     @Thunk Point mThumbOffset = new Point(-1, -1);
55     @Thunk Paint mThumbPaint;
56     private int mThumbMinWidth;
57     private int mThumbMaxWidth;
58     @Thunk int mThumbWidth;
59     @Thunk int mThumbHeight;
60     private int mThumbCurvature;
61     private Path mThumbPath = new Path();
62     private Paint mTrackPaint;
63     private int mTrackWidth;
64     private float mLastTouchY;
65     // 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;
68     private boolean mIsThumbDetached;
69     private boolean mCanThumbDetach;
70     private boolean mIgnoreDragGesture;
71 
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 
BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res)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));
84         mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
85         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();
89         mThumbPaint.setAntiAlias(true);
90         mThumbPaint.setColor(mThumbInactiveColor);
91         mThumbPaint.setStyle(Paint.Style.FILL);
92         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);
95         mThumbCurvature = mThumbMaxWidth - mThumbMinWidth;
96         mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
97     }
98 
setDetachThumbOnFastScroll()99     public void setDetachThumbOnFastScroll() {
100         mCanThumbDetach = true;
101     }
102 
reattachThumbToScroll()103     public void reattachThumbToScroll() {
104         mIsThumbDetached = false;
105     }
106 
setThumbOffset(int x, int y)107     public void setThumbOffset(int x, int y) {
108         if (mThumbOffset.x == x && mThumbOffset.y == y) {
109             return;
110         }
111         mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
112                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
113         mThumbOffset.set(x, y);
114         updateThumbPath();
115         mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
116                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
117         mRv.invalidate(mInvalidateRect);
118     }
119 
getThumbOffset()120     public Point getThumbOffset() {
121         return mThumbOffset;
122     }
123 
124     // Setter/getter for the thumb bar width for animations
setThumbWidth(int width)125     public void setThumbWidth(int width) {
126         mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
127                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
128         mThumbWidth = width;
129         updateThumbPath();
130         mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
131                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
132         mRv.invalidate(mInvalidateRect);
133     }
134 
getThumbWidth()135     public int getThumbWidth() {
136         return mThumbWidth;
137     }
138 
139     // Setter/getter for the track bar width for animations
setTrackWidth(int width)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());
147         mRv.invalidate(mInvalidateRect);
148     }
149 
getTrackWidth()150     public int getTrackWidth() {
151         return mTrackWidth;
152     }
153 
getThumbHeight()154     public int getThumbHeight() {
155         return mThumbHeight;
156     }
157 
getThumbMaxWidth()158     public int getThumbMaxWidth() {
159         return mThumbMaxWidth;
160     }
161 
getLastTouchY()162     public float getLastTouchY() {
163         return mLastTouchY;
164     }
165 
isDraggingThumb()166     public boolean isDraggingThumb() {
167         return mIsDragging;
168     }
169 
isThumbDetached()170     public boolean isThumbDetached() {
171         return mIsThumbDetached;
172     }
173 
174     /**
175      * Handles the touch event and determines whether to show the fast scroller (or updates it if
176      * it is already showing).
177      */
handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY)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:
185                 if (isNearThumb(downX, downY)) {
186                     mTouchOffset = downY - mThumbOffset.y;
187                 }
188                 break;
189             case MotionEvent.ACTION_MOVE:
190                 // 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();
193                 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
194                         isNearThumb(downX, lastY) &&
195                         Math.abs(y - downY) > config.getScaledTouchSlop()) {
196                     mRv.getParent().requestDisallowInterceptTouchEvent(true);
197                     mIsDragging = true;
198                     if (mCanThumbDetach) {
199                         mIsThumbDetached = true;
200                     }
201                     mTouchOffset += (lastY - downY);
202                     mPopup.animateVisibility(true);
203                     showActiveScrollbar(true);
204                 }
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());
214                     mRv.invalidate(mPopup.updateFastScrollerBounds(lastY));
215                     mLastTouchY = boundedY;
216                 }
217                 break;
218             case MotionEvent.ACTION_UP:
219             case MotionEvent.ACTION_CANCEL:
220                 mTouchOffset = 0;
221                 mLastTouchY = 0;
222                 mIgnoreDragGesture = false;
223                 if (mIsDragging) {
224                     mIsDragging = false;
225                     mPopup.animateVisibility(false);
226                     showActiveScrollbar(false);
227                 }
228                 break;
229         }
230     }
231 
draw(Canvas canvas)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         }
241         canvas.drawPath(mThumbPath, mThumbPaint);
242 
243         // Draw the popup
244         mPopup.draw(canvas);
245     }
246 
247     /**
248      * Animates the width and color of the scrollbar.
249      */
showActiveScrollbar(boolean isScrolling)250     private void showActiveScrollbar(boolean isScrolling) {
251         if (mScrollbarAnimator != null) {
252             mScrollbarAnimator.cancel();
253         }
254 
255         mScrollbarAnimator = new AnimatorSet();
256         ObjectAnimator trackWidthAnim = ObjectAnimator.ofInt(this, "trackWidth",
257                 isScrolling ? mThumbMaxWidth : mThumbMinWidth);
258         ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth",
259                 isScrolling ? mThumbMaxWidth : mThumbMinWidth);
260         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         }
274         mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
275         mScrollbarAnimator.start();
276     }
277 
278     /**
279      * Updates the path for the thumb drawable.
280      */
updateThumbPath()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     /**
294      * Returns whether the specified points are near the scroll bar bounds.
295      */
isNearThumb(int x, int y)296     private boolean isNearThumb(int x, int y) {
297         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 }
303