• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ViewConfiguration;
31 
32 import com.android.launcher3.util.Thunk;
33 
34 /**
35  * The track and scrollbar that shows when you scroll the list.
36  */
37 public class BaseRecyclerViewFastScrollBar {
38 
39     public interface FastScrollFocusableView {
setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated)40         void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated);
41     }
42 
43     private final static int MAX_TRACK_ALPHA = 30;
44     private final static int SCROLL_BAR_VIS_DURATION = 150;
45 
46     @Thunk BaseRecyclerView mRv;
47     private BaseRecyclerViewFastScrollPopup mPopup;
48 
49     private AnimatorSet mScrollbarAnimator;
50 
51     private int mThumbInactiveColor;
52     private int mThumbActiveColor;
53     @Thunk Point mThumbOffset = new Point(-1, -1);
54     @Thunk Paint mThumbPaint;
55     private int mThumbMinWidth;
56     private int mThumbMaxWidth;
57     @Thunk int mThumbWidth;
58     @Thunk int mThumbHeight;
59     private int mThumbCurvature;
60     private Path mThumbPath = new Path();
61     private Paint mTrackPaint;
62     private int mTrackWidth;
63     private float mLastTouchY;
64     // 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;
67     private boolean mIsThumbDetached;
68     private boolean mCanThumbDetach;
69     private boolean mIgnoreDragGesture;
70 
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 
BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res)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));
83         mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
84         mThumbActiveColor = mThumbInactiveColor = Utilities.getColorAccent(rv.getContext());
85         mThumbPaint = new Paint();
86         mThumbPaint.setAntiAlias(true);
87         mThumbPaint.setColor(mThumbInactiveColor);
88         mThumbPaint.setStyle(Paint.Style.FILL);
89         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);
92         mThumbCurvature = mThumbMaxWidth - mThumbMinWidth;
93         mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
94     }
95 
setDetachThumbOnFastScroll()96     public void setDetachThumbOnFastScroll() {
97         mCanThumbDetach = true;
98     }
99 
reattachThumbToScroll()100     public void reattachThumbToScroll() {
101         mIsThumbDetached = false;
102     }
103 
setThumbOffset(int x, int y)104     public void setThumbOffset(int x, int y) {
105         if (mThumbOffset.x == x && mThumbOffset.y == y) {
106             return;
107         }
108         mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
109                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
110         mThumbOffset.set(x, y);
111         updateThumbPath();
112         mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
113                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
114         mRv.invalidate(mInvalidateRect);
115     }
116 
getThumbOffset()117     public Point getThumbOffset() {
118         return mThumbOffset;
119     }
120 
121     // Setter/getter for the thumb bar width for animations
setThumbWidth(int width)122     public void setThumbWidth(int width) {
123         mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
124                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
125         mThumbWidth = width;
126         updateThumbPath();
127         mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
128                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
129         mRv.invalidate(mInvalidateRect);
130     }
131 
getThumbWidth()132     public int getThumbWidth() {
133         return mThumbWidth;
134     }
135 
136     // Setter/getter for the track bar width for animations
setTrackWidth(int width)137     public void setTrackWidth(int width) {
138         mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
139                 mRv.getVisibleHeight());
140         mTrackWidth = width;
141         updateThumbPath();
142         mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
143                 mRv.getVisibleHeight());
144         mRv.invalidate(mInvalidateRect);
145     }
146 
getTrackWidth()147     public int getTrackWidth() {
148         return mTrackWidth;
149     }
150 
getThumbHeight()151     public int getThumbHeight() {
152         return mThumbHeight;
153     }
154 
getThumbMaxWidth()155     public int getThumbMaxWidth() {
156         return mThumbMaxWidth;
157     }
158 
isDraggingThumb()159     public boolean isDraggingThumb() {
160         return mIsDragging;
161     }
162 
isThumbDetached()163     public boolean isThumbDetached() {
164         return mIsThumbDetached;
165     }
166 
167     /**
168      * Handles the touch event and determines whether to show the fast scroller (or updates it if
169      * it is already showing).
170      */
handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY)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:
178                 if (isNearThumb(downX, downY)) {
179                     mTouchOffset = downY - mThumbOffset.y;
180                 }
181                 break;
182             case MotionEvent.ACTION_MOVE:
183                 // 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();
186                 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
187                         isNearThumb(downX, lastY) &&
188                         Math.abs(y - downY) > config.getScaledTouchSlop()) {
189                     mRv.getParent().requestDisallowInterceptTouchEvent(true);
190                     mIsDragging = true;
191                     if (mCanThumbDetach) {
192                         mIsThumbDetached = true;
193                     }
194                     mTouchOffset += (lastY - downY);
195                     mPopup.animateVisibility(true);
196                     showActiveScrollbar(true);
197                 }
198                 if (mIsDragging) {
199                     // Update the fastscroller section name at this touch position
200                     int top = mRv.getBackgroundPadding().top;
201                     int bottom = top + mRv.getVisibleHeight() - mThumbHeight;
202                     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());
207                     mRv.invalidate(mPopup.updateFastScrollerBounds(lastY));
208                     mLastTouchY = boundedY;
209                     setThumbOffset(mRv.getScrollBarX(), (int) mLastTouchY);
210                 }
211                 break;
212             case MotionEvent.ACTION_UP:
213             case MotionEvent.ACTION_CANCEL:
214                 mTouchOffset = 0;
215                 mLastTouchY = 0;
216                 mIgnoreDragGesture = false;
217                 if (mIsDragging) {
218                     mIsDragging = false;
219                     mPopup.animateVisibility(false);
220                     showActiveScrollbar(false);
221                 }
222                 break;
223         }
224     }
225 
draw(Canvas canvas)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) {
233             canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth,
234                     mRv.getVisibleHeight(), mTrackPaint);
235         }
236         canvas.drawPath(mThumbPath, mThumbPaint);
237 
238         // Draw the popup
239         mPopup.draw(canvas);
240     }
241 
242     /**
243      * Animates the width and color of the scrollbar.
244      */
showActiveScrollbar(boolean isScrolling)245     private void showActiveScrollbar(boolean isScrolling) {
246         if (mScrollbarAnimator != null) {
247             mScrollbarAnimator.cancel();
248         }
249 
250         mScrollbarAnimator = new AnimatorSet();
251         ObjectAnimator trackWidthAnim = ObjectAnimator.ofInt(this, "trackWidth",
252                 isScrolling ? mThumbMaxWidth : mThumbMinWidth);
253         ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth",
254                 isScrolling ? mThumbMaxWidth : mThumbMinWidth);
255         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         }
269         mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
270         mScrollbarAnimator.start();
271     }
272 
273     /**
274      * Updates the path for the thumb drawable.
275      */
updateThumbPath()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     /**
289      * Returns whether the specified points are near the scroll bar bounds.
290      */
isNearThumb(int x, int y)291     public boolean isNearThumb(int x, int y) {
292         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 }
298