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 android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
20 
21 import android.content.Context;
22 import android.util.AttributeSet;
23 import android.util.Log;
24 import android.view.MotionEvent;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.view.accessibility.AccessibilityNodeInfo;
28 
29 import androidx.recyclerview.widget.RecyclerView;
30 
31 import com.android.launcher3.compat.AccessibilityManagerCompat;
32 import com.android.launcher3.testing.TestProtocol;
33 import com.android.launcher3.views.ActivityContext;
34 import com.android.launcher3.views.RecyclerViewFastScroller;
35 
36 
37 /**
38  * A base {@link RecyclerView}, which does the following:
39  * <ul>
40  *   <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
41  *   <li> Enable fast scroller.
42  * </ul>
43  */
44 public abstract class BaseRecyclerView extends RecyclerView  {
45 
46     protected RecyclerViewFastScroller mScrollbar;
47 
BaseRecyclerView(Context context)48     public BaseRecyclerView(Context context) {
49         this(context, null);
50     }
51 
BaseRecyclerView(Context context, AttributeSet attrs)52     public BaseRecyclerView(Context context, AttributeSet attrs) {
53         this(context, attrs, 0);
54     }
55 
BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)56     public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
57         super(context, attrs, defStyleAttr);
58     }
59 
60     @Override
onAttachedToWindow()61     protected void onAttachedToWindow() {
62         super.onAttachedToWindow();
63         bindFastScrollbar();
64     }
65 
bindFastScrollbar()66     public void bindFastScrollbar() {
67         ViewGroup parent = (ViewGroup) getParent().getParent();
68         mScrollbar = parent.findViewById(R.id.fast_scroller);
69         mScrollbar.setRecyclerView(this, parent.findViewById(R.id.fast_scroller_popup));
70         onUpdateScrollbar(0);
71     }
72 
getScrollbar()73     public RecyclerViewFastScroller getScrollbar() {
74         return mScrollbar;
75     }
76 
getScrollBarTop()77     public int getScrollBarTop() {
78         return getPaddingTop();
79     }
80 
81     /**
82      * Returns the height of the fast scroll bar
83      */
getScrollbarTrackHeight()84     public int getScrollbarTrackHeight() {
85         return mScrollbar.getHeight() - getScrollBarTop() - getPaddingBottom();
86     }
87 
88     /**
89      * Returns the available scroll height:
90      *   AvailableScrollHeight = Total height of the all items - last page height
91      */
getAvailableScrollHeight()92     protected abstract int getAvailableScrollHeight();
93 
94     /**
95      * Returns the available scroll bar height:
96      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
97      */
getAvailableScrollBarHeight()98     protected int getAvailableScrollBarHeight() {
99         int availableScrollBarHeight = getScrollbarTrackHeight() - mScrollbar.getThumbHeight();
100         return availableScrollBarHeight;
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         if (getCurrentScrollY() == 0) {
145             return true;
146         }
147         return getAdapter() == null || getAdapter().getItemCount() == 0;
148     }
149 
150     /**
151      * @return whether fast scrolling is supported in the current state.
152      */
supportsFastScrolling()153     public boolean supportsFastScrolling() {
154         return true;
155     }
156 
157     /**
158      * Maps the touch (from 0..1) to the adapter position that should be visible.
159      * <p>Override in each subclass of this base class.
160      *
161      * @return the scroll top of this recycler view.
162      */
getCurrentScrollY()163     public abstract int getCurrentScrollY();
164 
165     /**
166      * Maps the touch (from 0..1) to the adapter position that should be visible.
167      * <p>Override in each subclass of this base class.
168      */
scrollToPositionAtProgress(float touchFraction)169     public abstract String scrollToPositionAtProgress(float touchFraction);
170 
171     /**
172      * Updates the bounds for the scrollbar.
173      * <p>Override in each subclass of this base class.
174      */
onUpdateScrollbar(int dy)175     public abstract void onUpdateScrollbar(int dy);
176 
177     /**
178      * <p>Override in each subclass of this base class.
179      */
onFastScrollCompleted()180     public void onFastScrollCompleted() {}
181 
182     @Override
onScrollStateChanged(int state)183     public void onScrollStateChanged(int state) {
184         super.onScrollStateChanged(state);
185 
186         if (state == SCROLL_STATE_IDLE) {
187             AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
188         }
189     }
190 
191     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)192     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
193         super.onInitializeAccessibilityNodeInfo(info);
194         if (isLayoutSuppressed()) info.setScrollable(false);
195     }
196 
197     @Override
setLayoutFrozen(boolean frozen)198     public void setLayoutFrozen(boolean frozen) {
199         final boolean changing = frozen != isLayoutSuppressed();
200         super.setLayoutFrozen(frozen);
201         if (changing) {
202             ActivityContext.lookupContext(getContext()).getDragLayer()
203                     .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
204         }
205     }
206 }