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