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.support.v7.widget.RecyclerView;
22 import android.util.AttributeSet;
23 import android.view.MotionEvent;
24 import android.view.ViewGroup;
25 
26 import com.android.launcher3.util.Thunk;
27 
28 
29 /**
30  * A base {@link RecyclerView}, which does the following:
31  * <ul>
32  *   <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
33  *   <li> Enable fast scroller.
34  * </ul>
35  */
36 public abstract class BaseRecyclerView extends RecyclerView
37         implements RecyclerView.OnItemTouchListener {
38 
39     private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
40 
41     /** Keeps the last known scrolling delta/velocity along y-axis. */
42     @Thunk int mDy = 0;
43     private float mDeltaThreshold;
44 
45     protected final BaseRecyclerViewFastScrollBar mScrollbar;
46 
47     private int mDownX;
48     private int mDownY;
49     private int mLastY;
50 
BaseRecyclerView(Context context)51     public BaseRecyclerView(Context context) {
52         this(context, null);
53     }
54 
BaseRecyclerView(Context context, AttributeSet attrs)55     public BaseRecyclerView(Context context, AttributeSet attrs) {
56         this(context, attrs, 0);
57     }
58 
BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)59     public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
60         super(context, attrs, defStyleAttr);
61         mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
62         mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources());
63 
64         ScrollListener listener = new ScrollListener();
65         setOnScrollListener(listener);
66     }
67 
68     private class ScrollListener extends OnScrollListener {
ScrollListener()69         public ScrollListener() {
70             // Do nothing
71         }
72 
73         @Override
onScrolled(RecyclerView recyclerView, int dx, int dy)74         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
75             mDy = dy;
76 
77             // TODO(winsonc): If we want to animate the section heads while scrolling, we can
78             //                initiate that here if the recycler view scroll state is not
79             //                RecyclerView.SCROLL_STATE_IDLE.
80 
81             onUpdateScrollbar(dy);
82         }
83     }
84 
reset()85     public void reset() {
86         mScrollbar.reattachThumbToScroll();
87     }
88 
89     @Override
onFinishInflate()90     protected void onFinishInflate() {
91         super.onFinishInflate();
92         addOnItemTouchListener(this);
93     }
94 
95     @Override
onAttachedToWindow()96     protected void onAttachedToWindow() {
97         super.onAttachedToWindow();
98         mScrollbar.setPopupView(((ViewGroup) getParent()).findViewById(R.id.fast_scroller_popup));
99     }
100 
101     /**
102      * We intercept the touch handling only to support fast scrolling when initiated from the
103      * scroll bar.  Otherwise, we fall back to the default RecyclerView touch handling.
104      */
105     @Override
onInterceptTouchEvent(RecyclerView rv, MotionEvent ev)106     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
107         return handleTouchEvent(ev);
108     }
109 
110     @Override
onTouchEvent(RecyclerView rv, MotionEvent ev)111     public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
112         handleTouchEvent(ev);
113     }
114 
115     /**
116      * Handles the touch event and determines whether to show the fast scroller (or updates it if
117      * it is already showing).
118      */
handleTouchEvent(MotionEvent ev)119     private boolean handleTouchEvent(MotionEvent ev) {
120         int action = ev.getAction();
121         int x = (int) ev.getX();
122         int y = (int) ev.getY();
123         switch (action) {
124             case MotionEvent.ACTION_DOWN:
125                 // Keep track of the down positions
126                 mDownX = x;
127                 mDownY = mLastY = y;
128                 if (shouldStopScroll(ev)) {
129                     stopScroll();
130                 }
131                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
132                 break;
133             case MotionEvent.ACTION_MOVE:
134                 mLastY = y;
135                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
136                 break;
137             case MotionEvent.ACTION_UP:
138             case MotionEvent.ACTION_CANCEL:
139                 onFastScrollCompleted();
140                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
141                 break;
142         }
143         return mScrollbar.isDraggingThumb();
144     }
145 
onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)146     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
147         // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS
148     }
149 
150     /**
151      * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
152      */
shouldStopScroll(MotionEvent ev)153     protected boolean shouldStopScroll(MotionEvent ev) {
154         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
155             if ((Math.abs(mDy) < mDeltaThreshold &&
156                     getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
157                 // now the touch events are being passed to the {@link WidgetCell} until the
158                 // touch sequence goes over the touch slop.
159                 return true;
160             }
161         }
162         return false;
163     }
164 
165     /**
166      * Returns the height of the fast scroll bar
167      */
getScrollbarTrackHeight()168     protected int getScrollbarTrackHeight() {
169         return getHeight();
170     }
171 
172     /**
173      * Returns the available scroll height:
174      *   AvailableScrollHeight = Total height of the all items - last page height
175      */
getAvailableScrollHeight()176     protected abstract int getAvailableScrollHeight();
177 
178     /**
179      * Returns the available scroll bar height:
180      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
181      */
getAvailableScrollBarHeight()182     protected int getAvailableScrollBarHeight() {
183         int availableScrollBarHeight = getScrollbarTrackHeight() - mScrollbar.getThumbHeight();
184         return availableScrollBarHeight;
185     }
186 
187     /**
188      * Returns the track color (ignoring alpha), can be overridden by each subclass.
189      */
getFastScrollerTrackColor(int defaultTrackColor)190     public int getFastScrollerTrackColor(int defaultTrackColor) {
191         return defaultTrackColor;
192     }
193 
194     /**
195      * Returns the scrollbar for this recycler view.
196      */
getScrollBar()197     public BaseRecyclerViewFastScrollBar getScrollBar() {
198         return mScrollbar;
199     }
200 
201     @Override
dispatchDraw(Canvas canvas)202     protected void dispatchDraw(Canvas canvas) {
203         super.dispatchDraw(canvas);
204         onUpdateScrollbar(0);
205         mScrollbar.draw(canvas);
206     }
207 
208     /**
209      * Updates the scrollbar thumb offset to match the visible scroll of the recycler view.  It does
210      * this by mapping the available scroll area of the recycler view to the available space for the
211      * scroll bar.
212      *
213      * @param scrollY the current scroll y
214      */
synchronizeScrollBarThumbOffsetToViewScroll(int scrollY, int availableScrollHeight)215     protected void synchronizeScrollBarThumbOffsetToViewScroll(int scrollY,
216             int availableScrollHeight) {
217         // Only show the scrollbar if there is height to be scrolled
218         if (availableScrollHeight <= 0) {
219             mScrollbar.setThumbOffsetY(-1);
220             return;
221         }
222 
223         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
224         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
225         // padding)
226         int scrollBarY =
227                 (int) (((float) scrollY / availableScrollHeight) * getAvailableScrollBarHeight());
228 
229         // Calculate the position and size of the scroll bar
230         mScrollbar.setThumbOffsetY(scrollBarY);
231     }
232 
233     /**
234      * @return whether fast scrolling is supported in the current state.
235      */
supportsFastScrolling()236     protected boolean supportsFastScrolling() {
237         return true;
238     }
239 
240     /**
241      * Maps the touch (from 0..1) to the adapter position that should be visible.
242      * <p>Override in each subclass of this base class.
243      *
244      * @return the scroll top of this recycler view.
245      */
getCurrentScrollY()246     public abstract int getCurrentScrollY();
247 
248     /**
249      * Maps the touch (from 0..1) to the adapter position that should be visible.
250      * <p>Override in each subclass of this base class.
251      */
scrollToPositionAtProgress(float touchFraction)252     protected abstract String scrollToPositionAtProgress(float touchFraction);
253 
254     /**
255      * Updates the bounds for the scrollbar.
256      * <p>Override in each subclass of this base class.
257      */
onUpdateScrollbar(int dy)258     protected abstract void onUpdateScrollbar(int dy);
259 
260     /**
261      * <p>Override in each subclass of this base class.
262      */
onFastScrollCompleted()263     protected void onFastScrollCompleted() {}
264 }