• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 height of a given row (they are currently all the same height)
56         public int rowHeight;
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 available scroll height:
191      *   AvailableScrollHeight = Total height of the all items - last page height
192      *
193      * This assumes that all rows are the same height.
194      */
getAvailableScrollHeight(int rowCount, int rowHeight)195     protected int getAvailableScrollHeight(int rowCount, int rowHeight) {
196         int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
197         int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom();
198         int availableScrollHeight = scrollHeight - visibleHeight;
199         return availableScrollHeight;
200     }
201 
202     /**
203      * Returns the available scroll bar height:
204      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
205      */
getAvailableScrollBarHeight()206     protected int getAvailableScrollBarHeight() {
207         int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
208         int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight();
209         return availableScrollBarHeight;
210     }
211 
212     /**
213      * Returns the track color (ignoring alpha), can be overridden by each subclass.
214      */
getFastScrollerTrackColor(int defaultTrackColor)215     public int getFastScrollerTrackColor(int defaultTrackColor) {
216         return defaultTrackColor;
217     }
218 
219     /**
220      * Returns the inactive thumb color, can be overridden by each subclass.
221      */
getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor)222     public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) {
223         return defaultInactiveThumbColor;
224     }
225 
226     @Override
dispatchDraw(Canvas canvas)227     protected void dispatchDraw(Canvas canvas) {
228         super.dispatchDraw(canvas);
229         onUpdateScrollbar(0);
230         mScrollbar.draw(canvas);
231     }
232 
233     /**
234      * Updates the scrollbar thumb offset to match the visible scroll of the recycler view.  It does
235      * this by mapping the available scroll area of the recycler view to the available space for the
236      * scroll bar.
237      *
238      * @param scrollPosState the current scroll position
239      * @param rowCount the number of rows, used to calculate the total scroll height (assumes that
240      *                 all rows are the same height)
241      */
synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, int rowCount)242     protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState,
243             int rowCount) {
244         // Only show the scrollbar if there is height to be scrolled
245         int availableScrollBarHeight = getAvailableScrollBarHeight();
246         int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight);
247         if (availableScrollHeight <= 0) {
248             mScrollbar.setThumbOffset(-1, -1);
249             return;
250         }
251 
252         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
253         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
254         // padding)
255         int scrollY = getPaddingTop() +
256                 (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
257         int scrollBarY = mBackgroundPadding.top +
258                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
259 
260         // Calculate the position and size of the scroll bar
261         int scrollBarX;
262         if (Utilities.isRtl(getResources())) {
263             scrollBarX = mBackgroundPadding.left;
264         } else {
265             scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
266         }
267         mScrollbar.setThumbOffset(scrollBarX, scrollBarY);
268     }
269 
270     /**
271      * Maps the touch (from 0..1) to the adapter position that should be visible.
272      * <p>Override in each subclass of this base class.
273      */
scrollToPositionAtProgress(float touchFraction)274     public abstract String scrollToPositionAtProgress(float touchFraction);
275 
276     /**
277      * Updates the bounds for the scrollbar.
278      * <p>Override in each subclass of this base class.
279      */
onUpdateScrollbar(int dy)280     public abstract void onUpdateScrollbar(int dy);
281 
282     /**
283      * <p>Override in each subclass of this base class.
284      */
onFastScrollCompleted()285     public void onFastScrollCompleted() {}
286 
287     /**
288      * Returns information about the item that the recycler view is currently scrolled to.
289      */
getCurScrollState(ScrollPositionState stateOut)290     protected abstract void getCurScrollState(ScrollPositionState stateOut);
291 }