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.ObjectAnimator;
19 import android.content.res.Resources;
20 import android.graphics.Canvas;
21 import android.graphics.Color;
22 import android.graphics.Paint;
23 import android.graphics.Path;
24 import android.graphics.Rect;
25 import android.util.Property;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewConfiguration;
29 import android.widget.TextView;
30 
31 import com.android.launcher3.config.FeatureFlags;
32 import com.android.launcher3.util.Themes;
33 
34 /**
35  * The track and scrollbar that shows when you scroll the list.
36  */
37 public class BaseRecyclerViewFastScrollBar {
38 
39     private static final Property<BaseRecyclerViewFastScrollBar, Integer> TRACK_WIDTH =
40             new Property<BaseRecyclerViewFastScrollBar, Integer>(Integer.class, "width") {
41 
42                 @Override
43                 public Integer get(BaseRecyclerViewFastScrollBar scrollBar) {
44                     return scrollBar.mWidth;
45                 }
46 
47                 @Override
48                 public void set(BaseRecyclerViewFastScrollBar scrollBar, Integer value) {
49                     scrollBar.setTrackWidth(value);
50                 }
51             };
52 
53     private final static int MAX_TRACK_ALPHA = 30;
54     private final static int SCROLL_BAR_VIS_DURATION = 150;
55     private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
56 
57     private final Rect mTmpRect = new Rect();
58     private final BaseRecyclerView mRv;
59 
60     private final boolean mIsRtl;
61 
62     // The inset is the buffer around which a point will still register as a click on the scrollbar
63     private final int mTouchInset;
64 
65     private final int mMinWidth;
66     private final int mMaxWidth;
67 
68     // Current width of the track
69     private int mWidth;
70     private ObjectAnimator mWidthAnimator;
71 
72     private final Path mThumbPath = new Path();
73     private final Paint mThumbPaint;
74     private final int mThumbHeight;
75 
76     private final Paint mTrackPaint;
77 
78     private float mLastTouchY;
79     private boolean mIsDragging;
80     private boolean mIsThumbDetached;
81     private boolean mCanThumbDetach;
82     private boolean mIgnoreDragGesture;
83 
84     // This is the offset from the top of the scrollbar when the user first starts touching.  To
85     // prevent jumping, this offset is applied as the user scrolls.
86     private int mTouchOffsetY;
87     private int mThumbOffsetY;
88 
89     // Fast scroller popup
90     private TextView mPopupView;
91     private boolean mPopupVisible;
92     private String mPopupSectionName;
93 
BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res)94     public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
95         mRv = rv;
96         mTrackPaint = new Paint();
97         mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
98         mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
99 
100         mThumbPaint = new Paint();
101         mThumbPaint.setAntiAlias(true);
102         mThumbPaint.setColor(Themes.getColorAccent(rv.getContext()));
103         mThumbPaint.setStyle(Paint.Style.FILL);
104 
105         mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
106         mMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
107         mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
108         mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
109         mIsRtl = Utilities.isRtl(res);
110         updateThumbPath();
111     }
112 
setPopupView(View popup)113     public void setPopupView(View popup) {
114         mPopupView = (TextView) popup;
115     }
116 
setDetachThumbOnFastScroll()117     public void setDetachThumbOnFastScroll() {
118         mCanThumbDetach = true;
119     }
120 
reattachThumbToScroll()121     public void reattachThumbToScroll() {
122         mIsThumbDetached = false;
123     }
124 
getDrawLeft()125     private int getDrawLeft() {
126         return mIsRtl ? 0 : (mRv.getWidth() - mMaxWidth);
127     }
128 
setThumbOffsetY(int y)129     public void setThumbOffsetY(int y) {
130         if (mThumbOffsetY == y) {
131             return;
132         }
133 
134         // Invalidate the previous and new thumb area
135         int drawLeft = getDrawLeft();
136         mTmpRect.set(drawLeft, mThumbOffsetY, drawLeft + mMaxWidth, mThumbOffsetY + mThumbHeight);
137         mThumbOffsetY = y;
138         mTmpRect.union(drawLeft, mThumbOffsetY, drawLeft + mMaxWidth, mThumbOffsetY + mThumbHeight);
139         mRv.invalidate(mTmpRect);
140     }
141 
getThumbOffsetY()142     public int getThumbOffsetY() {
143         return mThumbOffsetY;
144     }
145 
setTrackWidth(int width)146     private void setTrackWidth(int width) {
147         if (mWidth == width) {
148             return;
149         }
150         int left = getDrawLeft();
151         // Invalidate the whole scroll bar area.
152         mRv.invalidate(left, 0, left + mMaxWidth, mRv.getScrollbarTrackHeight());
153 
154         mWidth = width;
155         updateThumbPath();
156     }
157 
158     /**
159      * Updates the path for the thumb drawable.
160      */
updateThumbPath()161     private void updateThumbPath() {
162         int smallWidth = mIsRtl ? mWidth : -mWidth;
163         int largeWidth = mIsRtl ? mMaxWidth : -mMaxWidth;
164 
165         mThumbPath.reset();
166         mThumbPath.moveTo(0, 0);
167         mThumbPath.lineTo(0, mThumbHeight);             // Left edge
168         mThumbPath.lineTo(smallWidth, mThumbHeight);    // bottom edge
169         mThumbPath.cubicTo(smallWidth, mThumbHeight,    // right edge
170                 largeWidth, mThumbHeight / 2,
171                 smallWidth, 0);
172         mThumbPath.close();
173     }
174 
getThumbHeight()175     public int getThumbHeight() {
176         return mThumbHeight;
177     }
178 
isDraggingThumb()179     public boolean isDraggingThumb() {
180         return mIsDragging;
181     }
182 
isThumbDetached()183     public boolean isThumbDetached() {
184         return mIsThumbDetached;
185     }
186 
187     /**
188      * Handles the touch event and determines whether to show the fast scroller (or updates it if
189      * it is already showing).
190      */
handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY)191     public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) {
192         ViewConfiguration config = ViewConfiguration.get(mRv.getContext());
193 
194         int action = ev.getAction();
195         int y = (int) ev.getY();
196         switch (action) {
197             case MotionEvent.ACTION_DOWN:
198                 if (isNearThumb(downX, downY)) {
199                     mTouchOffsetY = downY - mThumbOffsetY;
200                 } else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL
201                         && mRv.supportsFastScrolling()
202                         && isNearScrollBar(downX)) {
203                     calcTouchOffsetAndPrepToFastScroll(downY, lastY);
204                     updateFastScrollSectionNameAndThumbOffset(lastY, y);
205                 }
206                 break;
207             case MotionEvent.ACTION_MOVE:
208                 // Check if we should start scrolling, but ignore this fastscroll gesture if we have
209                 // exceeded some fixed movement
210                 mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop();
211                 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
212                         isNearThumb(downX, lastY) &&
213                         Math.abs(y - downY) > config.getScaledTouchSlop()) {
214                     calcTouchOffsetAndPrepToFastScroll(downY, lastY);
215                 }
216                 if (mIsDragging) {
217                     updateFastScrollSectionNameAndThumbOffset(lastY, y);
218                 }
219                 break;
220             case MotionEvent.ACTION_UP:
221             case MotionEvent.ACTION_CANCEL:
222                 mTouchOffsetY = 0;
223                 mLastTouchY = 0;
224                 mIgnoreDragGesture = false;
225                 if (mIsDragging) {
226                     mIsDragging = false;
227                     animatePopupVisibility(false);
228                     showActiveScrollbar(false);
229                 }
230                 break;
231         }
232     }
233 
calcTouchOffsetAndPrepToFastScroll(int downY, int lastY)234     private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) {
235         mRv.getParent().requestDisallowInterceptTouchEvent(true);
236         mIsDragging = true;
237         if (mCanThumbDetach) {
238             mIsThumbDetached = true;
239         }
240         mTouchOffsetY += (lastY - downY);
241         animatePopupVisibility(true);
242         showActiveScrollbar(true);
243     }
244 
updateFastScrollSectionNameAndThumbOffset(int lastY, int y)245     private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) {
246         // Update the fastscroller section name at this touch position
247         int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight;
248         float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY));
249         String sectionName = mRv.scrollToPositionAtProgress(boundedY / bottom);
250         if (!sectionName.equals(mPopupSectionName)) {
251             mPopupSectionName = sectionName;
252             mPopupView.setText(sectionName);
253         }
254         animatePopupVisibility(!sectionName.isEmpty());
255         updatePopupY(lastY);
256         mLastTouchY = boundedY;
257         setThumbOffsetY((int) mLastTouchY);
258     }
259 
draw(Canvas canvas)260     public void draw(Canvas canvas) {
261         if (mThumbOffsetY < 0) {
262             return;
263         }
264         int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
265         if (!mIsRtl) {
266             canvas.translate(mRv.getWidth(), 0);
267         }
268         // Draw the track
269         int thumbWidth = mIsRtl ? mWidth : -mWidth;
270         canvas.drawRect(0, 0, thumbWidth, mRv.getScrollbarTrackHeight(), mTrackPaint);
271 
272         canvas.translate(0, mThumbOffsetY);
273         canvas.drawPath(mThumbPath, mThumbPaint);
274         canvas.restoreToCount(saveCount);
275     }
276 
277     /**
278      * Animates the width of the scrollbar.
279      */
showActiveScrollbar(boolean isScrolling)280     private void showActiveScrollbar(boolean isScrolling) {
281         if (mWidthAnimator != null) {
282             mWidthAnimator.cancel();
283         }
284 
285         mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH,
286                 isScrolling ? mMaxWidth : mMinWidth);
287         mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
288         mWidthAnimator.start();
289     }
290 
291     /**
292      * Returns whether the specified point is inside the thumb bounds.
293      */
isNearThumb(int x, int y)294     public boolean isNearThumb(int x, int y) {
295         int left = getDrawLeft();
296         mTmpRect.set(left, mThumbOffsetY, left + mMaxWidth, mThumbOffsetY + mThumbHeight);
297         mTmpRect.inset(mTouchInset, mTouchInset);
298         return mTmpRect.contains(x, y);
299     }
300 
301     /**
302      * Returns whether the specified x position is near the scroll bar.
303      */
isNearScrollBar(int x)304     public boolean isNearScrollBar(int x) {
305         int left = getDrawLeft();
306         return x >= left && x <= left + mMaxWidth;
307     }
308 
animatePopupVisibility(boolean visible)309     private void animatePopupVisibility(boolean visible) {
310         if (mPopupVisible != visible) {
311             mPopupVisible = visible;
312             mPopupView.animate().cancel();
313             mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start();
314         }
315     }
316 
updatePopupY(int lastTouchY)317     private void updatePopupY(int lastTouchY) {
318         int height = mPopupView.getHeight();
319         float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height);
320         top = Math.max(mMaxWidth, Math.min(top, mRv.getScrollbarTrackHeight() - mMaxWidth - height));
321         mPopupView.setTranslationY(top);
322     }
323 }
324