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.util.AttributeSet;
21 import android.view.MotionEvent;
22 import android.view.View;
23 import android.view.ViewGroup;
24 
25 import com.android.launcher3.compat.AccessibilityManagerCompat;
26 import com.android.launcher3.views.RecyclerViewFastScroller;
27 
28 import androidx.recyclerview.widget.RecyclerView;
29 
30 
31 /**
32  * A base {@link RecyclerView}, which does the following:
33  * <ul>
34  *   <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
35  *   <li> Enable fast scroller.
36  * </ul>
37  */
38 public abstract class BaseRecyclerView extends RecyclerView  {
39 
40     protected RecyclerViewFastScroller mScrollbar;
41 
BaseRecyclerView(Context context)42     public BaseRecyclerView(Context context) {
43         this(context, null);
44     }
45 
BaseRecyclerView(Context context, AttributeSet attrs)46     public BaseRecyclerView(Context context, AttributeSet attrs) {
47         this(context, attrs, 0);
48     }
49 
BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)50     public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
51         super(context, attrs, defStyleAttr);
52     }
53 
54     @Override
onAttachedToWindow()55     protected void onAttachedToWindow() {
56         super.onAttachedToWindow();
57         bindFastScrollbar();
58     }
59 
bindFastScrollbar()60     public void bindFastScrollbar() {
61         ViewGroup parent = (ViewGroup) getParent().getParent();
62         mScrollbar = parent.findViewById(R.id.fast_scroller);
63         mScrollbar.setRecyclerView(this, parent.findViewById(R.id.fast_scroller_popup));
64         onUpdateScrollbar(0);
65     }
66 
getScrollbar()67     public RecyclerViewFastScroller getScrollbar() {
68         return mScrollbar;
69     }
70 
getScrollBarTop()71     public int getScrollBarTop() {
72         return getPaddingTop();
73     }
74 
75     /**
76      * Returns the height of the fast scroll bar
77      */
getScrollbarTrackHeight()78     public int getScrollbarTrackHeight() {
79         return mScrollbar.getHeight() - getScrollBarTop() - getPaddingBottom();
80     }
81 
82     /**
83      * Returns the available scroll height:
84      *   AvailableScrollHeight = Total height of the all items - last page height
85      */
getAvailableScrollHeight()86     protected abstract int getAvailableScrollHeight();
87 
88     /**
89      * Returns the available scroll bar height:
90      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
91      */
getAvailableScrollBarHeight()92     protected int getAvailableScrollBarHeight() {
93         int availableScrollBarHeight = getScrollbarTrackHeight() - mScrollbar.getThumbHeight();
94         return availableScrollBarHeight;
95     }
96 
97     /**
98      * Updates the scrollbar thumb offset to match the visible scroll of the recycler view.  It does
99      * this by mapping the available scroll area of the recycler view to the available space for the
100      * scroll bar.
101      *
102      * @param scrollY the current scroll y
103      */
synchronizeScrollBarThumbOffsetToViewScroll(int scrollY, int availableScrollHeight)104     protected void synchronizeScrollBarThumbOffsetToViewScroll(int scrollY,
105             int availableScrollHeight) {
106         // Only show the scrollbar if there is height to be scrolled
107         if (availableScrollHeight <= 0) {
108             mScrollbar.setThumbOffsetY(-1);
109             return;
110         }
111 
112         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
113         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
114         // padding)
115         int scrollBarY =
116                 (int) (((float) scrollY / availableScrollHeight) * getAvailableScrollBarHeight());
117 
118         // Calculate the position and size of the scroll bar
119         mScrollbar.setThumbOffsetY(scrollBarY);
120     }
121 
122     /**
123      * Returns whether the view itself will handle the touch event or not.
124      * @param ev MotionEvent in {@param eventSource}
125      */
shouldContainerScroll(MotionEvent ev, View eventSource)126     public boolean shouldContainerScroll(MotionEvent ev, View eventSource) {
127         float[] point = new float[2];
128         point[0] = ev.getX();
129         point[1] = ev.getY();
130         Utilities.mapCoordInSelfToDescendant(mScrollbar, eventSource, point);
131         // IF the MotionEvent is inside the thumb, container should not be pulled down.
132         if (mScrollbar.shouldBlockIntercept((int) point[0], (int) point[1])) {
133             return false;
134         }
135 
136         // IF scroller is at the very top OR there is no scroll bar because there is probably not
137         // enough items to scroll, THEN it's okay for the container to be pulled down.
138         if (getCurrentScrollY() == 0) {
139             return true;
140         }
141         return false;
142     }
143 
144     /**
145      * @return whether fast scrolling is supported in the current state.
146      */
supportsFastScrolling()147     public boolean supportsFastScrolling() {
148         return true;
149     }
150 
151     /**
152      * Maps the touch (from 0..1) to the adapter position that should be visible.
153      * <p>Override in each subclass of this base class.
154      *
155      * @return the scroll top of this recycler view.
156      */
getCurrentScrollY()157     public abstract int getCurrentScrollY();
158 
159     /**
160      * Maps the touch (from 0..1) to the adapter position that should be visible.
161      * <p>Override in each subclass of this base class.
162      */
scrollToPositionAtProgress(float touchFraction)163     public abstract String scrollToPositionAtProgress(float touchFraction);
164 
165     /**
166      * Updates the bounds for the scrollbar.
167      * <p>Override in each subclass of this base class.
168      */
onUpdateScrollbar(int dy)169     public abstract void onUpdateScrollbar(int dy);
170 
171     /**
172      * <p>Override in each subclass of this base class.
173      */
onFastScrollCompleted()174     public void onFastScrollCompleted() {}
175 
176     @Override
onScrollStateChanged(int state)177     public void onScrollStateChanged(int state) {
178         super.onScrollStateChanged(state);
179 
180         if (state == SCROLL_STATE_IDLE) {
181             AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
182         }
183     }
184 }