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