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