• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 
17 package com.android.launcher3.views;
18 
19 import android.animation.ObjectAnimator;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Paint;
25 import android.graphics.Point;
26 import android.graphics.Rect;
27 import android.support.v7.widget.RecyclerView;
28 import android.util.AttributeSet;
29 import android.util.Property;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.ViewConfiguration;
33 import android.widget.TextView;
34 
35 import com.android.launcher3.BaseRecyclerView;
36 import com.android.launcher3.R;
37 import com.android.launcher3.Utilities;
38 import com.android.launcher3.config.FeatureFlags;
39 import com.android.launcher3.graphics.FastScrollThumbDrawable;
40 import com.android.launcher3.util.Themes;
41 
42 /**
43  * The track and scrollbar that shows when you scroll the list.
44  */
45 public class RecyclerViewFastScroller extends View {
46 
47     private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
48     private static final Rect sTempRect = new Rect();
49 
50     private static final Property<RecyclerViewFastScroller, Integer> TRACK_WIDTH =
51             new Property<RecyclerViewFastScroller, Integer>(Integer.class, "width") {
52 
53                 @Override
54                 public Integer get(RecyclerViewFastScroller scrollBar) {
55                     return scrollBar.mWidth;
56                 }
57 
58                 @Override
59                 public void set(RecyclerViewFastScroller scrollBar, Integer value) {
60                     scrollBar.setTrackWidth(value);
61                 }
62             };
63 
64     private final static int MAX_TRACK_ALPHA = 30;
65     private final static int SCROLL_BAR_VIS_DURATION = 150;
66     private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 0.75f;
67 
68     private final int mMinWidth;
69     private final int mMaxWidth;
70     private final int mThumbPadding;
71 
72     /** Keeps the last known scrolling delta/velocity along y-axis. */
73     private int mDy = 0;
74     private final float mDeltaThreshold;
75 
76     private final ViewConfiguration mConfig;
77 
78     // Current width of the track
79     private int mWidth;
80     private ObjectAnimator mWidthAnimator;
81 
82     private final Paint mThumbPaint;
83     protected final int mThumbHeight;
84 
85     private final Paint mTrackPaint;
86 
87     private float mLastTouchY;
88     private boolean mIsDragging;
89     private boolean mIsThumbDetached;
90     private final boolean mCanThumbDetach;
91     private boolean mIgnoreDragGesture;
92 
93     // This is the offset from the top of the scrollbar when the user first starts touching.  To
94     // prevent jumping, this offset is applied as the user scrolls.
95     protected int mTouchOffsetY;
96     protected int mThumbOffsetY;
97 
98     // Fast scroller popup
99     private TextView mPopupView;
100     private boolean mPopupVisible;
101     private String mPopupSectionName;
102 
103     protected BaseRecyclerView mRv;
104     private RecyclerView.OnScrollListener mOnScrollListener;
105 
106     private int mDownX;
107     private int mDownY;
108     private int mLastY;
109 
RecyclerViewFastScroller(Context context)110     public RecyclerViewFastScroller(Context context) {
111         this(context, null);
112     }
113 
RecyclerViewFastScroller(Context context, AttributeSet attrs)114     public RecyclerViewFastScroller(Context context, AttributeSet attrs) {
115         this(context, attrs, 0);
116     }
117 
RecyclerViewFastScroller(Context context, AttributeSet attrs, int defStyleAttr)118     public RecyclerViewFastScroller(Context context, AttributeSet attrs, int defStyleAttr) {
119         super(context, attrs, defStyleAttr);
120 
121         mTrackPaint = new Paint();
122         mTrackPaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
123         mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
124 
125         mThumbPaint = new Paint();
126         mThumbPaint.setAntiAlias(true);
127         mThumbPaint.setColor(Themes.getColorAccent(context));
128         mThumbPaint.setStyle(Paint.Style.FILL);
129 
130         Resources res = getResources();
131         mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_min_width);
132         mMaxWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_max_width);
133 
134         mThumbPadding = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_padding);
135         mThumbHeight = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height);
136 
137         mConfig = ViewConfiguration.get(context);
138         mDeltaThreshold = res.getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
139 
140         TypedArray ta =
141                 context.obtainStyledAttributes(attrs, R.styleable.RecyclerViewFastScroller, defStyleAttr, 0);
142         mCanThumbDetach = ta.getBoolean(R.styleable.RecyclerViewFastScroller_canThumbDetach, false);
143         ta.recycle();
144     }
145 
setRecyclerView(BaseRecyclerView rv, TextView popupView)146     public void setRecyclerView(BaseRecyclerView rv, TextView popupView) {
147         if (mRv != null && mOnScrollListener != null) {
148             mRv.removeOnScrollListener(mOnScrollListener);
149         }
150         mRv = rv;
151 
152         mRv.addOnScrollListener(mOnScrollListener = new RecyclerView.OnScrollListener() {
153             @Override
154             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
155                 mDy = dy;
156 
157                 // TODO(winsonc): If we want to animate the section heads while scrolling, we can
158                 //                initiate that here if the recycler view scroll state is not
159                 //                RecyclerView.SCROLL_STATE_IDLE.
160 
161                 mRv.onUpdateScrollbar(dy);
162             }
163         });
164 
165         mPopupView = popupView;
166         mPopupView.setBackground(
167                 new FastScrollThumbDrawable(mThumbPaint, Utilities.isRtl(getResources())));
168     }
169 
reattachThumbToScroll()170     public void reattachThumbToScroll() {
171         mIsThumbDetached = false;
172     }
173 
setThumbOffsetY(int y)174     public void setThumbOffsetY(int y) {
175         if (mThumbOffsetY == y) {
176             return;
177         }
178         mThumbOffsetY = y;
179         invalidate();
180     }
181 
getThumbOffsetY()182     public int getThumbOffsetY() {
183         return mThumbOffsetY;
184     }
185 
setTrackWidth(int width)186     private void setTrackWidth(int width) {
187         if (mWidth == width) {
188             return;
189         }
190         mWidth = width;
191         invalidate();
192     }
193 
getThumbHeight()194     public int getThumbHeight() {
195         return mThumbHeight;
196     }
197 
isDraggingThumb()198     public boolean isDraggingThumb() {
199         return mIsDragging;
200     }
201 
isThumbDetached()202     public boolean isThumbDetached() {
203         return mIsThumbDetached;
204     }
205 
206     /**
207      * Handles the touch event and determines whether to show the fast scroller (or updates it if
208      * it is already showing).
209      */
handleTouchEvent(MotionEvent ev, Point offset)210     public boolean handleTouchEvent(MotionEvent ev, Point offset) {
211         int x = (int) ev.getX() - offset.x;
212         int y = (int) ev.getY() - offset.y;
213         switch (ev.getAction()) {
214             case MotionEvent.ACTION_DOWN:
215                 // Keep track of the down positions
216                 mDownX = x;
217                 mDownY = mLastY = y;
218 
219                 if ((Math.abs(mDy) < mDeltaThreshold &&
220                         mRv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
221                     // now the touch events are being passed to the {@link WidgetCell} until the
222                     // touch sequence goes over the touch slop.
223                     mRv.stopScroll();
224                 }
225                 if (isNearThumb(x, y)) {
226                     mTouchOffsetY = mDownY - mThumbOffsetY;
227                 } else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL
228                         && mRv.supportsFastScrolling()
229                         && isNearScrollBar(mDownX)) {
230                     calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
231                     updateFastScrollSectionNameAndThumbOffset(mLastY, y);
232                 }
233                 break;
234             case MotionEvent.ACTION_MOVE:
235                 mLastY = y;
236 
237                 // Check if we should start scrolling, but ignore this fastscroll gesture if we have
238                 // exceeded some fixed movement
239                 mIgnoreDragGesture |= Math.abs(y - mDownY) > mConfig.getScaledPagingTouchSlop();
240                 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
241                         isNearThumb(mDownX, mLastY) &&
242                         Math.abs(y - mDownY) > mConfig.getScaledTouchSlop()) {
243                     calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
244                 }
245                 if (mIsDragging) {
246                     updateFastScrollSectionNameAndThumbOffset(mLastY, y);
247                 }
248                 break;
249             case MotionEvent.ACTION_UP:
250             case MotionEvent.ACTION_CANCEL:
251                 mRv.onFastScrollCompleted();
252                 mTouchOffsetY = 0;
253                 mLastTouchY = 0;
254                 mIgnoreDragGesture = false;
255                 if (mIsDragging) {
256                     mIsDragging = false;
257                     animatePopupVisibility(false);
258                     showActiveScrollbar(false);
259                 }
260                 break;
261         }
262         return mIsDragging;
263     }
264 
calcTouchOffsetAndPrepToFastScroll(int downY, int lastY)265     private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) {
266         mIsDragging = true;
267         if (mCanThumbDetach) {
268             mIsThumbDetached = true;
269         }
270         mTouchOffsetY += (lastY - downY);
271         animatePopupVisibility(true);
272         showActiveScrollbar(true);
273     }
274 
updateFastScrollSectionNameAndThumbOffset(int lastY, int y)275     private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) {
276         // Update the fastscroller section name at this touch position
277         int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight;
278         float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY));
279         String sectionName = mRv.scrollToPositionAtProgress(boundedY / bottom);
280         if (!sectionName.equals(mPopupSectionName)) {
281             mPopupSectionName = sectionName;
282             mPopupView.setText(sectionName);
283         }
284         animatePopupVisibility(!sectionName.isEmpty());
285         updatePopupY(lastY);
286         mLastTouchY = boundedY;
287         setThumbOffsetY((int) mLastTouchY);
288     }
289 
onDraw(Canvas canvas)290     public void onDraw(Canvas canvas) {
291         if (mThumbOffsetY < 0) {
292             return;
293         }
294         int saveCount = canvas.save();
295         canvas.translate(getWidth() / 2, mRv.getScrollBarTop());
296         // Draw the track
297         float halfW = mWidth / 2;
298         canvas.drawRoundRect(-halfW, 0, halfW, mRv.getScrollbarTrackHeight(),
299                 mWidth, mWidth, mTrackPaint);
300 
301         canvas.translate(0, mThumbOffsetY);
302         halfW += mThumbPadding;
303         float r = mWidth + mThumbPadding + mThumbPadding;
304         canvas.drawRoundRect(-halfW, 0, halfW, mThumbHeight, r, r, mThumbPaint);
305         canvas.restoreToCount(saveCount);
306     }
307 
308 
309     /**
310      * Animates the width of the scrollbar.
311      */
showActiveScrollbar(boolean isScrolling)312     private void showActiveScrollbar(boolean isScrolling) {
313         if (mWidthAnimator != null) {
314             mWidthAnimator.cancel();
315         }
316 
317         mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH,
318                 isScrolling ? mMaxWidth : mMinWidth);
319         mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
320         mWidthAnimator.start();
321     }
322 
323     /**
324      * Returns whether the specified point is inside the thumb bounds.
325      */
isNearThumb(int x, int y)326     private boolean isNearThumb(int x, int y) {
327         int offset = y - mThumbOffsetY;
328 
329         return x >= 0 && x < getWidth() && offset >= 0 && offset <= mThumbHeight;
330     }
331 
332     /**
333      * Returns true if AllAppsTransitionController can handle vertical motion
334      * beginning at this point.
335      */
shouldBlockIntercept(int x, int y)336     public boolean shouldBlockIntercept(int x, int y) {
337         return isNearThumb(x, y);
338     }
339 
340     /**
341      * Returns whether the specified x position is near the scroll bar.
342      */
isNearScrollBar(int x)343     public boolean isNearScrollBar(int x) {
344         return x >= (getWidth() - mMaxWidth) / 2 && x <= (getWidth() + mMaxWidth) / 2;
345     }
346 
animatePopupVisibility(boolean visible)347     private void animatePopupVisibility(boolean visible) {
348         if (mPopupVisible != visible) {
349             mPopupVisible = visible;
350             mPopupView.animate().cancel();
351             mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start();
352         }
353     }
354 
updatePopupY(int lastTouchY)355     private void updatePopupY(int lastTouchY) {
356         int height = mPopupView.getHeight();
357         float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height)
358                 + mRv.getScrollBarTop();
359         top = Utilities.boundToRange(top,
360                 mMaxWidth, mRv.getScrollbarTrackHeight() - mMaxWidth - height);
361         mPopupView.setTranslationY(top);
362     }
363 
isHitInParent(float x, float y, Point outOffset)364     public boolean isHitInParent(float x, float y, Point outOffset) {
365         if (mThumbOffsetY < 0) {
366             return false;
367         }
368         getHitRect(sTempRect);
369         sTempRect.top += mRv.getScrollBarTop();
370         if (outOffset != null) {
371             outOffset.set(sTempRect.left, sTempRect.top);
372         }
373         return sTempRect.contains((int) x, (int) y);
374     }
375 
376     @Override
hasOverlappingRendering()377     public boolean hasOverlappingRendering() {
378         // There is actually some overlap between the track and the thumb. But since the track
379         // alpha is so low, it does not matter.
380         return false;
381     }
382 }
383