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 package com.android.launcher3.allapps;
17 
18 import android.support.v7.widget.RecyclerView;
19 import android.view.View;
20 
21 import com.android.launcher3.BaseRecyclerViewFastScrollBar;
22 import com.android.launcher3.BubbleTextView;
23 import com.android.launcher3.FastBitmapDrawable;
24 import com.android.launcher3.util.Thunk;
25 
26 import java.util.HashSet;
27 import java.util.List;
28 
29 public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback {
30 
31     private static final int INITIAL_TOUCH_SETTLING_DURATION = 100;
32     private static final int REPEAT_TOUCH_SETTLING_DURATION = 200;
33 
34     private AllAppsRecyclerView mRv;
35     private AlphabeticalAppsList mApps;
36 
37     // Keeps track of the current and targeted fast scroll section (the section to scroll to after
38     // the initial delay)
39     int mTargetFastScrollPosition = -1;
40     @Thunk String mCurrentFastScrollSection;
41     @Thunk String mTargetFastScrollSection;
42 
43     // The settled states affect the delay before the fast scroll animation is applied
44     private boolean mHasFastScrollTouchSettled;
45     private boolean mHasFastScrollTouchSettledAtLeastOnce;
46 
47     // Set of all views animated during fast scroll.  We keep track of these ourselves since there
48     // is no way to reset a view once it gets scrapped or recycled without other hacks
49     private HashSet<RecyclerView.ViewHolder> mTrackedFastScrollViews = new HashSet<>();
50 
51     // Smooth fast-scroll animation frames
52     @Thunk int mFastScrollFrameIndex;
53     @Thunk final int[] mFastScrollFrames = new int[10];
54 
55     /**
56      * This runnable runs a single frame of the smooth scroll animation and posts the next frame
57      * if necessary.
58      */
59     @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
60         @Override
61         public void run() {
62             if (mFastScrollFrameIndex < mFastScrollFrames.length) {
63                 mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
64                 mFastScrollFrameIndex++;
65                 mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
66             }
67         }
68     };
69 
70     /**
71      * This runnable updates the current fast scroll section to the target fastscroll section.
72      */
73     Runnable mFastScrollToTargetSectionRunnable = new Runnable() {
74         @Override
75         public void run() {
76             // Update to the target section
77             mCurrentFastScrollSection = mTargetFastScrollSection;
78             mHasFastScrollTouchSettled = true;
79             mHasFastScrollTouchSettledAtLeastOnce = true;
80             updateTrackedViewsFastScrollFocusState();
81         }
82     };
83 
AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps)84     public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) {
85         mRv = rv;
86         mApps = apps;
87     }
88 
onSetAdapter(AllAppsGridAdapter adapter)89     public void onSetAdapter(AllAppsGridAdapter adapter) {
90         adapter.setBindViewCallback(this);
91     }
92 
93     /**
94      * Smooth scrolls the recycler view to the given section.
95      *
96      * @return whether the fastscroller can scroll to the new section.
97      */
smoothScrollToSection(int scrollY, int availableScrollHeight, AlphabeticalAppsList.FastScrollSectionInfo info)98     public boolean smoothScrollToSection(int scrollY, int availableScrollHeight,
99             AlphabeticalAppsList.FastScrollSectionInfo info) {
100         if (mTargetFastScrollPosition != info.fastScrollToItem.position) {
101             mTargetFastScrollPosition = info.fastScrollToItem.position;
102             smoothSnapToPosition(scrollY, availableScrollHeight, info);
103             return true;
104         }
105         return false;
106     }
107 
108     /**
109      * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
110      * ourselves and animating the scroll on the recycler view.
111      */
smoothSnapToPosition(int scrollY, int availableScrollHeight, AlphabeticalAppsList.FastScrollSectionInfo info)112     private void smoothSnapToPosition(int scrollY, int availableScrollHeight,
113             AlphabeticalAppsList.FastScrollSectionInfo info) {
114         mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
115         mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
116 
117         trackAllChildViews();
118         if (mHasFastScrollTouchSettled) {
119             // In this case, the user has already settled once (and the fast scroll state has
120             // animated) and they are just fine-tuning their section from the last section, so
121             // we should make it feel fast and update immediately.
122             mCurrentFastScrollSection = info.sectionName;
123             mTargetFastScrollSection = null;
124             updateTrackedViewsFastScrollFocusState();
125         } else {
126             // Otherwise, the user has scrubbed really far, and we don't want to distract the user
127             // with the flashing fast scroll state change animation in addition to the fast scroll
128             // section popup, so reset the views to normal, and wait for the touch to settle again
129             // before animating the fast scroll state.
130             mCurrentFastScrollSection = null;
131             mTargetFastScrollSection = info.sectionName;
132             mHasFastScrollTouchSettled = false;
133             updateTrackedViewsFastScrollFocusState();
134 
135             // Delay scrolling to a new section until after some duration.  If the user has been
136             // scrubbing a while and makes multiple big jumps, then reduce the time needed for the
137             // fast scroll to settle so it doesn't feel so long.
138             mRv.postDelayed(mFastScrollToTargetSectionRunnable,
139                     mHasFastScrollTouchSettledAtLeastOnce ?
140                             REPEAT_TOUCH_SETTLING_DURATION :
141                             INITIAL_TOUCH_SETTLING_DURATION);
142         }
143 
144         // Calculate the full animation from the current scroll position to the final scroll
145         // position, and then run the animation for the duration.  If we are scrolling to the
146         // first fast scroll section, then just scroll to the top of the list itself.
147         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
148                 mApps.getFastScrollerSections();
149         int newPosition = info.fastScrollToItem.position;
150         int newScrollY = fastScrollSections.size() > 0 && fastScrollSections.get(0) == info
151                         ? 0
152                         : Math.min(availableScrollHeight, mRv.getCurrentScrollY(newPosition, 0));
153         int numFrames = mFastScrollFrames.length;
154         int deltaY = newScrollY - scrollY;
155         float ySign = Math.signum(deltaY);
156         int step = (int) (ySign * Math.ceil((float) Math.abs(deltaY) / numFrames));
157         for (int i = 0; i < numFrames; i++) {
158             // TODO(winsonc): We can interpolate this as well.
159             mFastScrollFrames[i] = (int) (ySign * Math.min(Math.abs(step), Math.abs(deltaY)));
160             deltaY -= step;
161         }
162         mFastScrollFrameIndex = 0;
163         mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
164     }
165 
onFastScrollCompleted()166     public void onFastScrollCompleted() {
167         // TODO(winsonc): Handle the case when the user scrolls and releases before the animation
168         //                runs
169 
170         // Stop animating the fast scroll position and state
171         mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
172         mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
173 
174         // Reset the tracking variables
175         mHasFastScrollTouchSettled = false;
176         mHasFastScrollTouchSettledAtLeastOnce = false;
177         mCurrentFastScrollSection = null;
178         mTargetFastScrollSection = null;
179         mTargetFastScrollPosition = -1;
180 
181         updateTrackedViewsFastScrollFocusState();
182         mTrackedFastScrollViews.clear();
183     }
184 
185     @Override
onBindView(AllAppsGridAdapter.ViewHolder holder)186     public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
187         // Update newly bound views to the current fast scroll state if we are fast scrolling
188         if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
189             mTrackedFastScrollViews.add(holder);
190         }
191     }
192 
193     /**
194      * Starts tracking all the recycler view's children which are FastScrollFocusableViews.
195      */
trackAllChildViews()196     private void trackAllChildViews() {
197         int childCount = mRv.getChildCount();
198         for (int i = 0; i < childCount; i++) {
199             RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder(mRv.getChildAt(i));
200             if (viewHolder != null) {
201                 mTrackedFastScrollViews.add(viewHolder);
202             }
203         }
204     }
205 
206     /**
207      * Updates the fast scroll focus on all the children.
208      */
updateTrackedViewsFastScrollFocusState()209     private void updateTrackedViewsFastScrollFocusState() {
210         for (RecyclerView.ViewHolder viewHolder : mTrackedFastScrollViews) {
211             int pos = viewHolder.getAdapterPosition();
212             boolean isActive = false;
213             if (mCurrentFastScrollSection != null && pos > -1) {
214                 AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
215                 isActive = item != null &&
216                         mCurrentFastScrollSection.equals(item.sectionName) &&
217                         item.position == mTargetFastScrollPosition;
218             }
219             viewHolder.itemView.setActivated(isActive);
220         }
221     }
222 }
223