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 
17 package com.android.launcher3;
18 
19 import android.content.Context;
20 import android.graphics.Canvas;
21 import android.graphics.Rect;
22 import android.support.v7.widget.RecyclerView;
23 import android.util.AttributeSet;
24 import android.view.MotionEvent;
25 import com.android.launcher3.util.Thunk;
26 
27 
28 /**
29  * A base {@link RecyclerView}, which does the following:
30  * <ul>
31  *   <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
32  *   <li> Enable fast scroller.
33  * </ul>
34  */
35 public abstract class BaseRecyclerView extends RecyclerView
36         implements RecyclerView.OnItemTouchListener {
37 
38     private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
39 
40     /** Keeps the last known scrolling delta/velocity along y-axis. */
41     @Thunk int mDy = 0;
42     private float mDeltaThreshold;
43 
44     /**
45      * The current scroll state of the recycler view.  We use this in onUpdateScrollbar()
46      * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
47      * that we can calculate what the scroll bar looks like, and where to jump to from the fast
48      * scroller.
49      */
50     public static class ScrollPositionState {
51         // The index of the first visible row
52         public int rowIndex;
53         // The offset of the first visible row
54         public int rowTopOffset;
55         // The adapter position of the first visible item
56         public int itemPos;
57     }
58 
59     protected BaseRecyclerViewFastScrollBar mScrollbar;
60 
61     private int mDownX;
62     private int mDownY;
63     private int mLastY;
64     protected Rect mBackgroundPadding = new Rect();
65 
BaseRecyclerView(Context context)66     public BaseRecyclerView(Context context) {
67         this(context, null);
68     }
69 
BaseRecyclerView(Context context, AttributeSet attrs)70     public BaseRecyclerView(Context context, AttributeSet attrs) {
71         this(context, attrs, 0);
72     }
73 
BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)74     public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
75         super(context, attrs, defStyleAttr);
76         mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
77         mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources());
78 
79         ScrollListener listener = new ScrollListener();
80         setOnScrollListener(listener);
81     }
82 
83     private class ScrollListener extends OnScrollListener {
ScrollListener()84         public ScrollListener() {
85             // Do nothing
86         }
87 
88         @Override
onScrolled(RecyclerView recyclerView, int dx, int dy)89         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
90             mDy = dy;
91 
92             // TODO(winsonc): If we want to animate the section heads while scrolling, we can
93             //                initiate that here if the recycler view scroll state is not
94             //                RecyclerView.SCROLL_STATE_IDLE.
95 
96             onUpdateScrollbar(dy);
97         }
98     }
99 
reset()100     public void reset() {
101         mScrollbar.reattachThumbToScroll();
102     }
103 
104     @Override
onFinishInflate()105     protected void onFinishInflate() {
106         super.onFinishInflate();
107         addOnItemTouchListener(this);
108     }
109 
110     /**
111      * We intercept the touch handling only to support fast scrolling when initiated from the
112      * scroll bar.  Otherwise, we fall back to the default RecyclerView touch handling.
113      */
114     @Override
onInterceptTouchEvent(RecyclerView rv, MotionEvent ev)115     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
116         return handleTouchEvent(ev);
117     }
118 
119     @Override
onTouchEvent(RecyclerView rv, MotionEvent ev)120     public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
121         handleTouchEvent(ev);
122     }
123 
124     /**
125      * Handles the touch event and determines whether to show the fast scroller (or updates it if
126      * it is already showing).
127      */
handleTouchEvent(MotionEvent ev)128     private boolean handleTouchEvent(MotionEvent ev) {
129         int action = ev.getAction();
130         int x = (int) ev.getX();
131         int y = (int) ev.getY();
132         switch (action) {
133             case MotionEvent.ACTION_DOWN:
134                 // Keep track of the down positions
135                 mDownX = x;
136                 mDownY = mLastY = y;
137                 if (shouldStopScroll(ev)) {
138                     stopScroll();
139                 }
140                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
141                 break;
142             case MotionEvent.ACTION_MOVE:
143                 mLastY = y;
144                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
145                 break;
146             case MotionEvent.ACTION_UP:
147             case MotionEvent.ACTION_CANCEL:
148                 onFastScrollCompleted();
149                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
150                 break;
151         }
152         return mScrollbar.isDraggingThumb();
153     }
154 
onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)155     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
156         // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS
157     }
158 
159     /**
160      * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
161      */
shouldStopScroll(MotionEvent ev)162     protected boolean shouldStopScroll(MotionEvent ev) {
163         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
164             if ((Math.abs(mDy) < mDeltaThreshold &&
165                     getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
166                 // now the touch events are being passed to the {@link WidgetCell} until the
167                 // touch sequence goes over the touch slop.
168                 return true;
169             }
170         }
171         return false;
172     }
173 
updateBackgroundPadding(Rect padding)174     public void updateBackgroundPadding(Rect padding) {
175         mBackgroundPadding.set(padding);
176     }
177 
getBackgroundPadding()178     public Rect getBackgroundPadding() {
179         return mBackgroundPadding;
180     }
181 
182     /**
183      * Returns the scroll bar width when the user is scrolling.
184      */
getMaxScrollbarWidth()185     public int getMaxScrollbarWidth() {
186         return mScrollbar.getThumbMaxWidth();
187     }
188 
189     /**
190      * Returns the visible height of the recycler view:
191      *   VisibleHeight = View height - top padding - bottom padding
192      */
getVisibleHeight()193     protected int getVisibleHeight() {
194         int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
195         return visibleHeight;
196     }
197 
198     /**
199      * Returns the available scroll height:
200      *   AvailableScrollHeight = Total height of the all items - last page height
201      */
getAvailableScrollHeight(int rowCount)202     protected int getAvailableScrollHeight(int rowCount) {
203         int totalHeight = getPaddingTop() + getTop(rowCount) + getPaddingBottom();
204         int availableScrollHeight = totalHeight - getVisibleHeight();
205         return availableScrollHeight;
206     }
207 
208     /**
209      * Returns the available scroll bar height:
210      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
211      */
getAvailableScrollBarHeight()212     protected int getAvailableScrollBarHeight() {
213         int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight();
214         return availableScrollBarHeight;
215     }
216 
217     /**
218      * Returns the track color (ignoring alpha), can be overridden by each subclass.
219      */
getFastScrollerTrackColor(int defaultTrackColor)220     public int getFastScrollerTrackColor(int defaultTrackColor) {
221         return defaultTrackColor;
222     }
223 
224     /**
225      * Returns the inactive thumb color, can be overridden by each subclass.
226      */
getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor)227     public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) {
228         return defaultInactiveThumbColor;
229     }
230 
231     /**
232      * Returns the scrollbar for this recycler view.
233      */
getScrollBar()234     public BaseRecyclerViewFastScrollBar getScrollBar() {
235         return mScrollbar;
236     }
237 
238     @Override
dispatchDraw(Canvas canvas)239     protected void dispatchDraw(Canvas canvas) {
240         super.dispatchDraw(canvas);
241         onUpdateScrollbar(0);
242         mScrollbar.draw(canvas);
243     }
244 
245     /**
246      * Updates the scrollbar thumb offset to match the visible scroll of the recycler view.  It does
247      * this by mapping the available scroll area of the recycler view to the available space for the
248      * scroll bar.
249      *
250      * @param scrollPosState the current scroll position
251      * @param rowCount the number of rows, used to calculate the total scroll height (assumes that
252      *                 all rows are the same height)
253      */
synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, int rowCount)254     protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState,
255             int rowCount) {
256         // Only show the scrollbar if there is height to be scrolled
257         int availableScrollBarHeight = getAvailableScrollBarHeight();
258         int availableScrollHeight = getAvailableScrollHeight(rowCount);
259         if (availableScrollHeight <= 0) {
260             mScrollbar.setThumbOffset(-1, -1);
261             return;
262         }
263 
264         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
265         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
266         // padding)
267         int scrollY = getScrollTop(scrollPosState);
268         int scrollBarY = mBackgroundPadding.top +
269                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
270 
271         // Calculate the position and size of the scroll bar
272         int scrollBarX;
273         if (Utilities.isRtl(getResources())) {
274             scrollBarX = mBackgroundPadding.left;
275         } else {
276             scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
277         }
278         mScrollbar.setThumbOffset(scrollBarX, scrollBarY);
279     }
280 
281     /**
282      * @return whether fast scrolling is supported in the current state.
283      */
supportsFastScrolling()284     protected boolean supportsFastScrolling() {
285         return true;
286     }
287 
288     /**
289      * Maps the touch (from 0..1) to the adapter position that should be visible.
290      * <p>Override in each subclass of this base class.
291      *
292      * @return the scroll top of this recycler view.
293      */
getScrollTop(ScrollPositionState scrollPosState)294     protected int getScrollTop(ScrollPositionState scrollPosState) {
295         return getPaddingTop() + getTop(scrollPosState.rowIndex) -
296                 scrollPosState.rowTopOffset;
297     }
298 
299     /**
300      * Returns information about the item that the recycler view is currently scrolled to.
301      */
getCurScrollState(ScrollPositionState stateOut, int viewTypeMask)302     protected abstract void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask);
303 
304     /**
305      * Returns the top (or y position) of the row at the specified index.
306      */
getTop(int rowIndex)307     protected abstract int getTop(int rowIndex);
308 
309     /**
310      * Maps the touch (from 0..1) to the adapter position that should be visible.
311      * <p>Override in each subclass of this base class.
312      */
scrollToPositionAtProgress(float touchFraction)313     protected abstract String scrollToPositionAtProgress(float touchFraction);
314 
315     /**
316      * Updates the bounds for the scrollbar.
317      * <p>Override in each subclass of this base class.
318      */
onUpdateScrollbar(int dy)319     protected abstract void onUpdateScrollbar(int dy);
320 
321     /**
322      * <p>Override in each subclass of this base class.
323      */
onFastScrollCompleted()324     protected void onFastScrollCompleted() {}
325 }