• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.FastBitmapDrawable;
23 import com.android.launcher3.util.Thunk;
24 
25 import java.util.HashSet;
26 import java.util.List;
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.  If we are scrolling to the
147         // first fast scroll section, then just scroll to the top of the list itself.
148         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
149                 mApps.getFastScrollerSections();
150         int newPosition = info.fastScrollToItem.position;
151         int newScrollY = fastScrollSections.size() > 0 && fastScrollSections.get(0) == info
152                         ? 0
153                         : Math.min(availableScrollHeight, mRv.getCurrentScrollY(newPosition, 0));
154         int numFrames = mFastScrollFrames.length;
155         int deltaY = newScrollY - scrollY;
156         float ySign = Math.signum(deltaY);
157         int step = (int) (ySign * Math.ceil((float) Math.abs(deltaY) / numFrames));
158         for (int i = 0; i < numFrames; i++) {
159             // TODO(winsonc): We can interpolate this as well.
160             mFastScrollFrames[i] = (int) (ySign * Math.min(Math.abs(step), Math.abs(deltaY)));
161             deltaY -= step;
162         }
163         mFastScrollFrameIndex = 0;
164         mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
165     }
166 
onFastScrollCompleted()167     public void onFastScrollCompleted() {
168         // TODO(winsonc): Handle the case when the user scrolls and releases before the animation
169         //                runs
170 
171         // Stop animating the fast scroll position and state
172         mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
173         mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
174 
175         // Reset the tracking variables
176         mHasFastScrollTouchSettled = false;
177         mHasFastScrollTouchSettledAtLeastOnce = false;
178         mCurrentFastScrollSection = null;
179         mTargetFastScrollSection = null;
180         mTargetFastScrollPosition = -1;
181 
182         updateTrackedViewsFastScrollFocusState();
183         mTrackedFastScrollViews.clear();
184     }
185 
186     @Override
onBindView(AllAppsGridAdapter.ViewHolder holder)187     public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
188         // Update newly bound views to the current fast scroll state if we are fast scrolling
189         if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
190             if (holder.mContent instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
191                 BaseRecyclerViewFastScrollBar.FastScrollFocusableView v =
192                         (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) holder.mContent;
193                 updateViewFastScrollFocusState(v, holder.getPosition(), false /* animated */);
194                 mTrackedFastScrollViews.add(v);
195             }
196         }
197     }
198 
199     /**
200      * Starts tracking all the recycler view's children which are FastScrollFocusableViews.
201      */
trackAllChildViews()202     private void trackAllChildViews() {
203         int childCount = mRv.getChildCount();
204         for (int i = 0; i < childCount; i++) {
205             View v = mRv.getChildAt(i);
206             if (v instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
207                 mTrackedFastScrollViews.add((BaseRecyclerViewFastScrollBar.FastScrollFocusableView) v);
208             }
209         }
210     }
211 
212     /**
213      * Updates the fast scroll focus on all the children.
214      */
updateTrackedViewsFastScrollFocusState()215     private void updateTrackedViewsFastScrollFocusState() {
216         for (BaseRecyclerViewFastScrollBar.FastScrollFocusableView v : mTrackedFastScrollViews) {
217             RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder((View) v);
218             int pos = (viewHolder != null) ? viewHolder.getPosition() : -1;
219             updateViewFastScrollFocusState(v, pos, true);
220         }
221     }
222 
223     /**
224      * Updates the fast scroll focus on all a given view.
225      */
updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v, int pos, boolean animated)226     private void updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v,
227                                                 int pos, boolean animated) {
228         FastBitmapDrawable.State newState = FastBitmapDrawable.State.NORMAL;
229         if (mCurrentFastScrollSection != null && pos > -1) {
230             AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
231             boolean highlight = item.sectionName.equals(mCurrentFastScrollSection) &&
232                     item.position == mTargetFastScrollPosition;
233             newState = highlight ?
234                     FastBitmapDrawable.State.FAST_SCROLL_HIGHLIGHTED :
235                     FastBitmapDrawable.State.FAST_SCROLL_UNHIGHLIGHTED;
236         }
237         v.setFastScrollFocusState(newState, animated);
238     }
239 }
240