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