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